From b9b2a51804656231395b71e7e3846df12cb90007 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sat, 6 Jul 2024 10:36:58 -0400 Subject: [PATCH 1/3] Add gltfpack to package file. --- js/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 9ff8f69..527f9eb 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,7 @@ { "dependencies": { - "gltf-pipeline": "^4.1.0" + "gltf-pipeline": "^4.1.0", + "gltfpack": "^0.21.0" }, "scripts": { "glue-ar-export": "npm install && node glue-ar-export.js" From c44ffdd64651a347d876e5d60fd45e8b191d3be8 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 7 Jul 2024 00:45:19 -0400 Subject: [PATCH 2/3] Reorganize package structure and add gltfpack as a compression option. --- glue_ar/__init__.py | 30 +++-- glue_ar/{tools => }/ar.svg | 0 glue_ar/{ => common}/common.py | 0 glue_ar/{ => common}/export.py | 44 +++++-- glue_ar/common/export_state.py | 44 +++++++ glue_ar/{ => common}/scatter.py | 0 .../scatter_export_options.py} | 2 +- glue_ar/{ => common}/volume.py | 0 .../volume_export_options.py} | 2 +- .../jupyter.py => jupyter/export_tool.py} | 9 +- glue_ar/{ => qt}/export_dialog.py | 47 +------ glue_ar/{ => qt}/export_dialog.ui | 27 ++-- glue_ar/qt/export_tool.py | 61 ++++++++++ glue_ar/{ => qt}/exporting_dialog.py | 0 glue_ar/{ => qt}/qr.py | 0 glue_ar/{ => qt}/qr_dialog.py | 2 +- glue_ar/{ => qt}/qr_dialog.ui | 0 glue_ar/qt/qr_tool.py | 61 ++++++++++ glue_ar/{ => qt}/server.py | 0 glue_ar/tools/__init__.py | 2 - glue_ar/tools/ar.png | Bin 17146 -> 0 bytes glue_ar/tools/qt.py | 115 ------------------ glue_ar/utils.py | 4 + 23 files changed, 249 insertions(+), 201 deletions(-) rename glue_ar/{tools => }/ar.svg (100%) rename glue_ar/{ => common}/common.py (100%) rename glue_ar/{ => common}/export.py (85%) create mode 100644 glue_ar/common/export_state.py rename glue_ar/{ => common}/scatter.py (100%) rename glue_ar/{export_scatter.py => common/scatter_export_options.py} (85%) rename glue_ar/{ => common}/volume.py (100%) rename glue_ar/{export_volume.py => common/volume_export_options.py} (87%) rename glue_ar/{tools/jupyter.py => jupyter/export_tool.py} (95%) rename glue_ar/{ => qt}/export_dialog.py (70%) rename glue_ar/{ => qt}/export_dialog.ui (91%) create mode 100644 glue_ar/qt/export_tool.py rename glue_ar/{ => qt}/exporting_dialog.py (100%) rename glue_ar/{ => qt}/qr.py (100%) rename glue_ar/{ => qt}/qr_dialog.py (94%) rename glue_ar/{ => qt}/qr_dialog.ui (100%) create mode 100644 glue_ar/qt/qr_tool.py rename glue_ar/{ => qt}/server.py (100%) delete mode 100644 glue_ar/tools/__init__.py delete mode 100644 glue_ar/tools/ar.png delete mode 100644 glue_ar/tools/qt.py diff --git a/glue_ar/__init__.py b/glue_ar/__init__.py index fd3fbdc..7689f4d 100644 --- a/glue_ar/__init__.py +++ b/glue_ar/__init__.py @@ -1,6 +1,5 @@ -from glue_ar.export_scatter import * # noqa -from glue_ar.export_volume import * # noqa -from glue_ar.tools import * # noqa +from glue_ar.common.scatter_export_options import * # noqa +from glue_ar.common.volume_export_options import * # noqa def setup_qt(): @@ -8,26 +7,35 @@ def setup_qt(): from glue_vispy_viewers.scatter.qt.scatter_viewer import VispyScatterViewer except ImportError: from glue_vispy_viewers.scatter.scatter_viewer import VispyScatterViewer - VispyScatterViewer.tools = [t for t in VispyScatterViewer.tools] + ["ar"] + + from glue_ar.qt.export_tool import QtARExportTool # noqa + VispyScatterViewer.subtools = { **VispyScatterViewer.subtools, "save": VispyScatterViewer.subtools["save"] + ["save:ar"] } - VispyScatterViewer.subtools["ar"] = ["ar:qr"] try: from glue_vispy_viewers.volume.qt.volume_viewer import VispyVolumeViewer except ImportError: from glue_vispy_viewers.volume.volume_viewer import VispyVolumeViewer - VispyVolumeViewer.tools = [t for t in VispyVolumeViewer.tools] + ["ar"] VispyVolumeViewer.subtools = { **VispyVolumeViewer.subtools, "save": VispyVolumeViewer.subtools["save"] + ["save:ar"] } - VispyVolumeViewer.subtools["ar"] = ["ar:qr"] + + try: + from glue_ar.qt.qr_tool import ARLocalQRTool # noqa + VispyScatterViewer.tools = [t for t in VispyScatterViewer.tools] + ["ar"] + VispyVolumeViewer.tools = [t for t in VispyVolumeViewer.tools] + ["ar"] + VispyScatterViewer.subtools["ar"] = ["ar:qr"] + VispyVolumeViewer.subtools["ar"] = ["ar:qr"] + except ImportError: + pass def setup_jupyter(): + from glue_ar.jupyter.export_tool import JupyterARExportTool # noqa from glue_vispy_viewers.scatter.jupyter import JupyterVispyScatterViewer from glue_vispy_viewers.volume.jupyter import JupyterVispyVolumeViewer JupyterVispyScatterViewer.tools = [t for t in JupyterVispyScatterViewer.tools] + ["save:ar_jupyter"] @@ -37,10 +45,14 @@ def setup_jupyter(): def setup(): try: setup_qt() - except ImportError: + except ImportError as e: + print("Qt setup error") + print(e) pass try: setup_jupyter() - except ImportError: + except ImportError as e: + print("Jupyter setup error") + print(e) pass diff --git a/glue_ar/tools/ar.svg b/glue_ar/ar.svg similarity index 100% rename from glue_ar/tools/ar.svg rename to glue_ar/ar.svg diff --git a/glue_ar/common.py b/glue_ar/common/common.py similarity index 100% rename from glue_ar/common.py rename to glue_ar/common/common.py diff --git a/glue_ar/export.py b/glue_ar/common/export.py similarity index 85% rename from glue_ar/export.py rename to glue_ar/common/export.py index b15b696..d939a42 100644 --- a/glue_ar/export.py +++ b/glue_ar/common/export.py @@ -8,14 +8,17 @@ from glue_vispy_viewers.scatter.scatter_viewer import Vispy3DScatterViewerState from glue_vispy_viewers.volume.layer_state import VolumeLayerState -from glue_ar.scatter import scatter_layer_as_multiblock +from glue_ar.common.scatter import scatter_layer_as_multiblock from glue_ar.utils import bounds_3d_from_layers, xyz_bounds -from glue_ar.volume import bounds_3d, meshes_for_volume_layer +from glue_ar.common.volume import bounds_3d, meshes_for_volume_layer -GLTF_PIPELINE_FILEPATH = join(dirname(abspath(__file__)), "js", - "node_modules", "gltf-pipeline", - "bin", "gltf-pipeline.js") +NODE_MODULES_DIR = join(abspath(join(dirname(abspath(__file__)), "..")), + "js", "node_modules") + + +GLTF_PIPELINE_FILEPATH = join(NODE_MODULES_DIR, "gltf-pipeline", "bin", "gltf-pipeline.js") +GLTFPACK_FILEPATH = join(NODE_MODULES_DIR, "gltfpack", "cli.js") def export_meshes(meshes, output_path): @@ -32,10 +35,27 @@ def export_meshes(meshes, output_path): raise ValueError("Unsupported extension!") -def compress_gl(filepath): +def compress_gltf_pipeline(filepath): run(["node", GLTF_PIPELINE_FILEPATH, "-i", filepath, "-o", filepath, "-d"], capture_output=True) +def compress_gltfpack(filepath): + run(["node", GLTFPACK_FILEPATH, "-i", filepath, "-o", filepath], capture_output=True) + + +COMPRESSORS = { + "draco": compress_gltf_pipeline, + "meshoptimizer": compress_gltfpack +} + + +def compress_gl(filepath, method="draco"): + compressor = COMPRESSORS.get(method, None) + if compressor is None: + raise ValueError("Invalid compression method specified") + compressor(filepath) + + def export_gl_by_extension(exporter, filepath): _, ext = splitext(filepath) if ext == ".glb": @@ -52,7 +72,7 @@ def export_gl_by_extension(exporter, filepath): # matters into our own hands. # We want alphaMode as BLEND # see https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#alpha-coverage -def export_gl(plotter, filepath, with_alpha=True, compress=True): +def export_gl(plotter, filepath, with_alpha=True, compression="draco"): path, ext = splitext(filepath) gltf_path = filepath glb = ext == ".glb" @@ -66,8 +86,8 @@ def export_gl(plotter, filepath, with_alpha=True, compress=True): for material in gl.model.materials: material.alphaMode = "BLEND" export_gl_by_extension(gl, filepath) - if compress: - compress_gl(filepath) + if compression != "none": + compress_gl(filepath, method=compression) if glb: remove(gltf_path) @@ -212,15 +232,13 @@ def create_plotter(viewer, state_dictionary): return plotter -def export_to_ar(viewer, filepath, state_dict, compress=True): +def export_to_ar(viewer, filepath, state_dict, compression="draco"): dir, base = split(filepath) name, ext = splitext(base) plotter = create_plotter(viewer, state_dict) html_path = join(dir, f"{name}.html") if ext in [".gltf", ".glb"]: - export_gl(plotter, filepath, with_alpha=True, compress=compress) - if compress: - compress_gl(filepath) + export_gl(plotter, filepath, with_alpha=True, compression=compression) export_modelviewer(html_path, base, viewer.state.title) else: plotter.export_obj(filepath) diff --git a/glue_ar/common/export_state.py b/glue_ar/common/export_state.py new file mode 100644 index 0000000..3252d3e --- /dev/null +++ b/glue_ar/common/export_state.py @@ -0,0 +1,44 @@ +from echo import SelectionCallbackProperty +from glue.config import DictRegistry +from glue.core.data_combo_helper import ComboHelper +from glue.core.state_objects import State + + +__all__ = ['ar_layer_export'] + + +class ARExportLayerOptionsRegistry(DictRegistry): + + def add(self, layer_state_cls, layer_options_state): + if not issubclass(layer_options_state, State): + raise ValueError("Layer options must be a glue State type") + self._members[layer_state_cls] = layer_options_state + + def __call__(self, layer_state_cls): + def adder(export_state_class): + self.add(layer_state_cls, export_state_class) + return adder + + +ar_layer_export = ARExportLayerOptionsRegistry() + + +class ARExportDialogState(State): + + filetype = SelectionCallbackProperty() + layer = SelectionCallbackProperty() + compression = SelectionCallbackProperty(True) + + def __init__(self, layers): + + super(ARExportDialogState, self).__init__() + + self.filetype_helper = ComboHelper(self, 'filetype') + self.filetype_helper.choices = ['glTF', 'glB', 'OBJ'] + + self.compression_helper = ComboHelper(self, 'compression') + self.compression_helper.choices = ['None', 'Draco', 'Meshoptimizer'] + + self.layers = layers + self.layer_helper = ComboHelper(self, 'layer') + self.layer_helper.choices = [state.layer.label for state in self.layers] diff --git a/glue_ar/scatter.py b/glue_ar/common/scatter.py similarity index 100% rename from glue_ar/scatter.py rename to glue_ar/common/scatter.py diff --git a/glue_ar/export_scatter.py b/glue_ar/common/scatter_export_options.py similarity index 85% rename from glue_ar/export_scatter.py rename to glue_ar/common/scatter_export_options.py index 7ab9ec7..a2f1f2d 100644 --- a/glue_ar/export_scatter.py +++ b/glue_ar/common/scatter_export_options.py @@ -1,6 +1,6 @@ from echo import CallbackProperty from glue.core.state_objects import State -from glue_ar.export_dialog import ar_layer_export +from glue_ar.common.export_state import ar_layer_export from glue_vispy_viewers.scatter.layer_state import ScatterLayerState __all__ = ["ARScatterExportOptions"] diff --git a/glue_ar/volume.py b/glue_ar/common/volume.py similarity index 100% rename from glue_ar/volume.py rename to glue_ar/common/volume.py diff --git a/glue_ar/export_volume.py b/glue_ar/common/volume_export_options.py similarity index 87% rename from glue_ar/export_volume.py rename to glue_ar/common/volume_export_options.py index 49eda6e..0d4b98c 100644 --- a/glue_ar/export_volume.py +++ b/glue_ar/common/volume_export_options.py @@ -1,6 +1,6 @@ from echo import CallbackProperty from glue.core.state_objects import State -from glue_ar.export_dialog import ar_layer_export +from glue_ar.common.export_state import ar_layer_export from glue_vispy_viewers.volume.layer_state import VolumeLayerState __all__ = ["ARVolumeExportOptions"] diff --git a/glue_ar/tools/jupyter.py b/glue_ar/jupyter/export_tool.py similarity index 95% rename from glue_ar/tools/jupyter.py rename to glue_ar/jupyter/export_tool.py index 97995a8..485de30 100644 --- a/glue_ar/tools/jupyter.py +++ b/glue_ar/jupyter/export_tool.py @@ -1,11 +1,11 @@ -import os from os import getcwd from os.path import exists from glue.config import viewer_tool from glue.viewers.common.tool import Tool -from glue_ar.export import export_to_ar +from glue_ar.common.export import export_to_ar +from glue_ar.utils import AR_ICON import ipyvuetify as v # noqa from ipywidgets import HBox, Layout # noqa @@ -15,9 +15,6 @@ __all__ = ["JupyterARExportTool"] -AR_ICON = os.path.abspath(os.path.join(os.path.dirname(__file__), "ar")) - - @viewer_tool class JupyterARExportTool(Tool): icon = AR_ICON @@ -92,4 +89,4 @@ def on_no_click(button, event, data): self.viewer.output_widget.clear_output() def save_figure(self, filepath): - export_to_ar(self.viewer, filepath, state_dict={}, compress=True) + export_to_ar(self.viewer, filepath, state_dict={}, compression="draco") diff --git a/glue_ar/export_dialog.py b/glue_ar/qt/export_dialog.py similarity index 70% rename from glue_ar/export_dialog.py rename to glue_ar/qt/export_dialog.py index 7e680f1..ce83c63 100644 --- a/glue_ar/export_dialog.py +++ b/glue_ar/qt/export_dialog.py @@ -1,58 +1,21 @@ import os -from echo import CallbackProperty, SelectionCallbackProperty from echo.qt import autoconnect_callbacks_to_qt, connect_checkable_button, connect_float_text - -from glue.config import DictRegistry -from glue.core.data_combo_helper import ComboHelper -from glue.core.state_objects import State from glue_qt.utils import load_ui +from glue_ar.common.export_state import ARExportDialogState, ar_layer_export + from qtpy.QtWidgets import QCheckBox, QDialog, QHBoxLayout, QLabel, QLineEdit from qtpy.QtGui import QIntValidator, QDoubleValidator -__all__ = ['ar_layer_export', 'ARExportDialog'] +__all__ = ['ARExportDialog'] def display_name(prop): return prop.replace("_", " ").capitalize() -class ARExportLayerOptionsRegistry(DictRegistry): - - def add(self, layer_state_cls, layer_options_state): - if not issubclass(layer_options_state, State): - raise ValueError("Layer options must be a glue State type") - self._members[layer_state_cls] = layer_options_state - - def __call__(self, layer_state_cls): - def adder(export_state_class): - self.add(layer_state_cls, export_state_class) - return adder - - -ar_layer_export = ARExportLayerOptionsRegistry() - - -class ARExportDialogState(State): - - filetype = SelectionCallbackProperty() - layer = SelectionCallbackProperty() - draco = CallbackProperty(True) - - def __init__(self, layers): - - super(ARExportDialogState, self).__init__() - - self.filetype_helper = ComboHelper(self, 'filetype') - self.filetype_helper.choices = ['glTF', 'glB', 'OBJ'] - - self.layers = layers - self.layer_helper = ComboHelper(self, 'layer') - self.layer_helper.choices = [state.layer.label for state in self.layers] - - class ARExportDialog(QDialog): def __init__(self, parent=None, viewer=None): @@ -99,6 +62,8 @@ def _widgets_for_property(self, instance, property, display_name): widget.setValidator(validator) self._layer_connections.append(connect_float_text(instance, property, widget)) return [label, widget] + else: + return [] def _clear_layout(self, layout): if layout is not None: @@ -127,4 +92,4 @@ def _update_layer_ui(self, layer): def _on_filetype_change(self, filetype): gl = filetype.lower() in ["gltf", "glb"] - self.ui.bool_draco.setVisible(gl) + self.ui.combosel_compression.setVisible(gl) diff --git a/glue_ar/export_dialog.ui b/glue_ar/qt/export_dialog.ui similarity index 91% rename from glue_ar/export_dialog.ui rename to glue_ar/qt/export_dialog.ui index 09a37a1..79fd251 100644 --- a/glue_ar/export_dialog.ui +++ b/glue_ar/qt/export_dialog.ui @@ -14,19 +14,16 @@ Export 3D Volume - - + + + + + - Use Draco compression + Set the export settings for each layer - - - - - - @@ -34,10 +31,13 @@ - - + + + + + - Set the export settings for each layer + Select compression method @@ -83,6 +83,9 @@ + + + diff --git a/glue_ar/qt/export_tool.py b/glue_ar/qt/export_tool.py new file mode 100644 index 0000000..2a3cf14 --- /dev/null +++ b/glue_ar/qt/export_tool.py @@ -0,0 +1,61 @@ +from os.path import splitext + +from qtpy import compat +from qtpy.QtWidgets import QDialog + +from glue.config import viewer_tool +from glue.viewers.common.tool import SimpleToolMenu, Tool +from glue_qt.utils.threading import Worker + +from glue_ar.utils import AR_ICON +from glue_ar.common.export import export_to_ar +from glue_ar.qt.export_dialog import ARExportDialog +from glue_ar.qt.exporting_dialog import ExportingDialog + + +__all__ = ["ARToolMenu", "QtARExportTool"] + +_FILETYPE_NAMES = { + ".obj": "OBJ", + ".gltf": "glTF", + ".glb": "glB" +} + + +@viewer_tool +class ARToolMenu(SimpleToolMenu): + tool_id = "ar" + icon = AR_ICON + tool_tip = "AR utilities" + + +@viewer_tool +class QtARExportTool(Tool): + icon = AR_ICON + tool_id = "save:ar" + action_text = "Export 3D file" + tool_tip = "Export the current view to a 3D file" + + _default_filename = "glue_export" + + def activate(self): + + dialog = ARExportDialog(parent=self.viewer, viewer=self.viewer) + result = dialog.exec_() + if result == QDialog.Rejected: + return + + export_path, _ = compat.getsavefilename(parent=self.viewer, + basedir=f"{self._default_filename}.{dialog.state.filetype.lower()}") + if not export_path: + return + + _, ext = splitext(export_path) + filetype = _FILETYPE_NAMES.get(ext, None) + worker = Worker(export_to_ar, self.viewer, export_path, dialog.state_dictionary, + compression=dialog.state.compression.lower()) + exporting_dialog = ExportingDialog(parent=self.viewer, filetype=filetype) + worker.result.connect(exporting_dialog.close) + worker.error.connect(exporting_dialog.close) + worker.start() + exporting_dialog.exec_() diff --git a/glue_ar/exporting_dialog.py b/glue_ar/qt/exporting_dialog.py similarity index 100% rename from glue_ar/exporting_dialog.py rename to glue_ar/qt/exporting_dialog.py diff --git a/glue_ar/qr.py b/glue_ar/qt/qr.py similarity index 100% rename from glue_ar/qr.py rename to glue_ar/qt/qr.py diff --git a/glue_ar/qr_dialog.py b/glue_ar/qt/qr_dialog.py similarity index 94% rename from glue_ar/qr_dialog.py rename to glue_ar/qt/qr_dialog.py index dea1ca9..c7f15eb 100644 --- a/glue_ar/qr_dialog.py +++ b/glue_ar/qt/qr_dialog.py @@ -5,7 +5,7 @@ from qtpy.QtWidgets import QDialog from qtpy.QtGui import QPixmap -from glue_ar.qr import create_qr +from glue_ar.qt.qr import create_qr class QRDialog(QDialog): diff --git a/glue_ar/qr_dialog.ui b/glue_ar/qt/qr_dialog.ui similarity index 100% rename from glue_ar/qr_dialog.ui rename to glue_ar/qt/qr_dialog.ui diff --git a/glue_ar/qt/qr_tool.py b/glue_ar/qt/qr_tool.py new file mode 100644 index 0000000..8079e45 --- /dev/null +++ b/glue_ar/qt/qr_tool.py @@ -0,0 +1,61 @@ +import ngrok +import os +from os.path import split +from tempfile import NamedTemporaryFile +from threading import Thread + +from glue.config import viewer_tool +from glue.viewers.common.tool import Tool + +from glue_ar.utils import AR_ICON +from glue_ar.common.export import create_plotter, export_gl, export_modelviewer +from glue_ar.qt.qr import get_local_ip +from glue_ar.qt.qr_dialog import QRDialog +from glue_ar.qt.server import run_ar_server + + +__all__ = ["ARLocalQRTool"] + + +@viewer_tool +class ARLocalQRTool(Tool): + icon = AR_ICON + tool_id = "ar:qr" + action_text = "3D view via QR" + tool_tip = "Get a QR code for the current view in 3D" + + def activate(self): + with NamedTemporaryFile(suffix=".gltf") as gltf_tmp, \ + NamedTemporaryFile(suffix=".html") as html_tmp: + + plotter = create_plotter(self.viewer, {}) + export_gl(plotter, gltf_tmp.name, with_alpha=True) + _, gltf_base = split(gltf_tmp.name) + export_modelviewer(html_tmp.name, gltf_base, self.viewer.state.title) + + port = 4000 + directory, filename = split(html_tmp.name) + server = run_ar_server(port, directory) + use_ngrok = os.getenv("NGROK_AUTHTOKEN", None) is not None + + try: + thread = Thread(target=server.serve_forever) + thread.start() + + if use_ngrok: + listener = ngrok.forward(port, authtoken_from_env=True) + url = f"{listener.url()}/{filename}" + else: + ip = get_local_ip() + url = f"http://{ip}:{port}/{filename}" + dialog = QRDialog(parent=self.viewer, url=url) + dialog.exec_() + + finally: + if use_ngrok: + try: + ngrok.disconnect(listener.url()) + except RuntimeError: + pass + server.shutdown() + server.server_close() diff --git a/glue_ar/server.py b/glue_ar/qt/server.py similarity index 100% rename from glue_ar/server.py rename to glue_ar/qt/server.py diff --git a/glue_ar/tools/__init__.py b/glue_ar/tools/__init__.py deleted file mode 100644 index 5d8835f..0000000 --- a/glue_ar/tools/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .jupyter import * # noqa -from .qt import * # noqa diff --git a/glue_ar/tools/ar.png b/glue_ar/tools/ar.png deleted file mode 100644 index 6657d0c7523bf0bf25595b3ece05bd9837163efa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17146 zcmeIa_dDEM7d|YB5)mazLe|Z0c@0Cj=a`1}p;`0&kjno#V0V5%)h^IPzP6qxDu|#TXfS>Y`ko*%$LP7v9{j*F$ z;(4EhWc4o+5}8yI66U`%>QRrt56CUlRNy2R#6LMrg-PHQ%D<6@?j$6_+{CX-FY^_= zz>5?fsu~E2rAuU__a#fmEu~0EU@NL{n9j?IjZDf^#*vY5$Cc-^`Y@{-2vRb*A{|=e zk|7174jszyE7=WL)NL4wj`{Hv3DZ|H)PO>as(Ngc=bOOIePEB$7I&&E6s-& z&YmOcJ?bL5a?_?GUk>H_4jFlc6ET={@=g;Umv)U&!o$_PcXHUV?&FW4C70b^*B?wcF{%cT3Fd$A4jb7%ED2K z1Sj;WbbJ^(B4y=Uha21b`cGCxD|~GNlrnlH?>^YMAfV6i&%*uV{lgnyold*g--daEm92zrfqK5Ut!EY#R;2G4j#=L`y$k76 zrqz9p?|ZxgHB$+-Vhdm_UV4tp(oB?89;Q#!t21LIC}?n))Y~ zE$oouTI6=BHyUv?C81~j+g$y}10>QynuAXwFX)~H&71UY7;bi%{E7(rOU8!R-E>4GcO2gT(;Wn9;<@x|^WPLrZ1x{Ynpb$I%1g|&X zAQv9Nf4hL(hbcSnd~~SzjfC`%;w5%mRy?C;ciw_>WGIdYE;JN9k>Fu6Y#a-#;Bwdc zEGrWZF-J+LQ)Be{Sgl0(727Xq%X!Bu^=eyV$CU#u4?c!)-O!`UiJs8Rr^8VcQRduVZuH#J z(kqINyXiPgDzOmm^<`&^|i_}kQklHl{tV;mZ zz%#AsEPfJIkIcV#Yl=AhfjL)yyTk1_DWv4^e1A%{!>~9bc3q4%%%p*91-^llsl&A( z9&@I-swX4PG%KM^(QeP*SH*NWIRw~0NgHlnt>Y#Qh_w65EAhhCE8L&~5{GT5|F4F0d$Z7$jjj~ts#*EjK!qySO-^Jf~d!da&1OMh#i{dLTXriX~ zRvNaX>v>dwVP?aB*l=cH-nV_vSp90_>Kg3kL7r+UE^9K+*O~8j8bw!j8XII-nNDTU z3<2zXhx6;=>z2nfkIO!1D!sWM^cooYhIylpk&{e#sYbM&C3yq({#M(xjbHoTU)W-t z>#dGD*H#*6Cm6Cl@WH>(G#Q=LUNI~TP{rv>RF!~WRSTXb)v;fzi?UU9YcRlQK+h^ z3@&1v(;pANqozNEJOj@XGeRMlQ}hR(eIPEa5&tlE@gtfsH7D#zC6eLz1(qK>b=^Mh z10oZb=Dd`vkZCronLdrMgrbNZytwi8vz7Z_xVJa|V$!&lcQ^&PCJlTKwVnIGk@)s4 z_AIjACZx5AZeIc}Ue8m4QE^8F@x~mJx@~?DNMn4wZ(@$Cij+XN#7Knx!^-(Ofmav0 zp*#HUI(DMB3U%fx0r9K;P4Cfc>ABfP2kS-_V2HggsK5}fP(^`N(O z)#?HuH);4E6gJV)A1V*qa;3T2Aw+z=zXl8h=w6_Ksc%xY7iSZ{DA7NKttsVuwxUH~ zgY=1UZFq33C3nZOp9-xm^$26gv`dM{QwAJQlfx>pPx%k8`qGMJInmHJ&s4GxaosMq zH5FFGfUIL(hhtE!Z;m+k=%I+pZyz5u8qTy#Hmh{x%oj(Czf*Lld4J0=W?k{zEawnE zt6Ll%_C|tIcmKt;vEBTDJ@!)__f>Lwegj8Au+j)!}POLy71_R+6o@fKz!48CJcsD{~p6*G# z7TtDOq|Sp0vbl!a$2bzwCfaxH4=IT6)Gw|I`TOEUl%z$BR058A>|&`--@P1J&(V4)4!!B7I*Z%<^+Rp|`cirOLSpD>Bda%1tvFa4S~ZOd&v*h$`U}5Qpp?%IJr3vT`d0I3 z8uJvo@*;8~q3QxQ=lBbTk^9VGaFpnLiSpbWiC@-z(r-Ow@RpQo>86sPgTP2yUkWVt zGd)(kdskrf1ucD3X)3d_a5(Nkr4WxP?u~}?M%9Bgl&TgoATCGh@lD=_mIhz&jkHo0@cX-K+*Dy@NSC|Erj}J=i^q$b&6{T;R2z{`#~tJKFTM_nDs=V!D+_L*dMx8}T6X7_wc8bRR%CIjdc3 z?#cD)I96>hUasV~OH`2dW;B`(r}I$pDXsp7pg7@gI?#nqKMbUOU->(5;@smQMO2`E zw&;}smm(I7`Xh3E<<ntIT;s!ze}}A(+WSa$$&$(l>>ZBLFiWk+QD1a48GxtzD3yusc1IAGjEJ=?HeMPaXBXt^9W?WgR;d2KIvs3 zvW%42r{`I7+);Swr-YM3bK{W{H$2BAFq=On!X`C?Ce|PoPhV3+aX{oo$VssfGP%JS zLs{_Tx8APksg63y;w4Khxx++Oh`s&dUFbU;UT>)G7W7x(*T(d2%$YBi25jUf!|~o3 zmPhL8UBZPqs!#9736FGJVmm8rIF8?qv+k_xB0$k+D}Y%Ln0Do(8Kcakc9gXsAM~ml zc&uoPyYq5Xoxx*Ja)Iwy=H~`ys?qNU?b;Hmi4aH6WoXPa+_jq5) zSJ#6$-ka(cX!S!SgEvKV$y3i3f_f}DpVVT9qGUKiTBpiV4=LTQf2LLXxtC4BJqtE( zmQ(qzU19*S>vsR^o&`IfWcn3I`#Dkp^cHNYhdce*G+!VUWou`>jW<^|0=5qgc%7UGig+^&A@ql~PgqnVm za0hBrZ;{0-`M4^TWxxOtQFCZgGV5MnNa;uzmeVkR2!OP0p%Xf9YMI# zuvjj!5NVM|&2V@ei@4pvFaKTha9vi?hI2MTtKU)Av)qi2bG#@kBF~qd9dAjKM4QT| z=&yW3g;+k2^|cY_RoO6$2u1QJZL!l(7;W^3xl}#+`32Ei>$!58MXcUn-%5r}Zee>1P zQ{@@jq6}TL&yG%=;Nfu@ZLJdXP@TxU#k+S}9y|@rQTfW_bXTVsMI)Xoq%paGZTPX8rEJb#J+9!fWw6+^E4STl6p}Y2 z@nJ1*xH8EKl^xk7&0S%D2Us@xUrwN_25rOxRQB>SV8ogHGJQDHZek1}0U3aj9Pk}D z*{wk<6MzaEHY@gA-5 zj#hAKFklURqPs4p8lM>bXc@Y_5_=uqi>QHI(2Vo`YxchG#rp!P8cF19zdi=bG*Q*! zSaIIYtyoHQIroYV#%qF|+cZ)54e+L3_sLC+JZhufVzj*R(|U79vhx{%az|W0aI;fn z{n19Ji0N^%VBh!SYQetx9ajB7$0cd~z@^UJveTJfs-I=&k+wDc zPLn7ZM(FWily9si%9fC$c1y2Y?cSm7B(N~$0j;*38G3hO&Q1vQa zvRBGaa4q)llp=G>jD{wH5RI@$fA0bDWh3}Yj>VqqHl(9?`K_1Wt3jE+cbX$zjMR|S z_@xzKx>_=SJGhc&JnYrTYGcIiRLF>Uk)76FQG)fW>+Kq*GidKsOfiWEoMY| zMlqC$CDD4afiLpA9jwp)+4ZAaRCh3w%Vr&kmDaX?>smHNtJ)Qs_>>2<*@c94%@bDW zIa%W7UmNRgjn|5o=ND&{A0elxP?{VRJ)ki5mOjJ|e zr8|sq39`i10hb#J!Br|CV`0NFJqdKX_jBKcSVrm?hDXx5H0?W9mfA%_OE4p!6+WQ1 za)uprooh#pb2C(5$y3GVPk)-h0o5VMF6E{i!z@0-sK!jcpI zf#h9$x4SAx#%!wDzbX|nebKkVu*^UE(&USk+(sYQ@ENd<3s;;B9Z&n>@klsX zzidugbv+C#3g&~GU%3e$`G>{Mb59dL`xtySXp78yzgAoaP%aUqxGD~mtr)W8Jf};Tr^o^F^|gsT+wrFmo;)n!%KIg%xJy z-k1#&D>a#9oVzr#$T+I%F}?o4_C5X| zh!xt_#<0C-OTwL_r&BCqWU+Q=bccvoE4u_0F}#^=rO$TK454 zN#(G?!A|Yg7+ez#ecu7FvDw0mA0F^_LP|0Fj@pHI=B<&~BuF#E;v?)I{|&6)o8TG= zBzG)aeaR%35<`fP&o-jXQ;%`6QbT$z_)8ZrEuv=@cP!4tbo#nDe_H>6_;q)nSzOuT z1<}7Em$Egc7hhG0J?>&wZrs;nl%^es6*d`eQQT#5E1qynRE8y*!Y&kX9{u|5ElR*R zYa|z_<$uEZ0S9z51ZRtTePypmbW34!8F(;3U`cT>zge%FHF2!8*6cw;w3k#tF{TgD zSh-G)j9clg({-vfi~dXt{$yD1{6BW8e7d?(kL`@|^ zN&+rozZv8Xe!eA`O*=hm%!G-94KoA8Z!l=)!QdC+@pxc*ulcf*`sWG%3RUw6kUCR9aslX1CNVCCkV3dV z(3;9Gc5e9ERVlwt^y&FSK4TZQlw_t|QfL#YO3(7!s^~IW{`H(YS2Xx{AiW5cHD_wQ zUMOF1vmVZ4pgh541rHfXZXXd=J-Ok@8v=KstzUnIEi48O6}NnMQ8KTE(ER7L!T!G5c!V#n~I{8E<7D z)P8zt^@F>%iO*o=uQJ9WzwE6oD(tU8`u~_&&AqZHe=;;f9`&c@6!Zz>kNX}~s6W$$ za1F&qxs?}|)}GSPS5{ig%=WNj7~&|&ob(5-BwT1`_;jSS?;l)IUYAU|({)UqOXy8& z)+>WUtw3*Ps6lLtB4v_b7CRKCffn62s1r)g)>kRv*a}fY`OHONusE{hWxq87%U8Ud zMY$OxENnh};ngW#h^m#454#?cp`q6URS0fI6?+Vgb$IWwLey)q&!S{nLRz;VtJ{$U zwy1tzS9S5|{7yzI@0+zVod?lAF6k;!%C721?%#DiTo$;aCDx+2g5bT{O*Z>xDNiwy zNcjbSclDt{MPJFT5rPJK;dQ&rANWvy(4XxEZ~FWN;u>X(QYaFk+fOKIq4>slN-m0! z5G!aed|$QywtFDjN1ti`b4tfRl;6M!pK%PKJYTD_K5!S`&ehoB^z!6RM@!C_a_`#L z1cK0x9BAZJc^aVZnx|G1+O~)Q0y~EbCDa@PQdY~@mCg?rsU=gQK>u% z({t+s`Uu#lD&9_bH9L9zahCEg0}J_Ne18IDq484ke1TkAAl?L@q8Pb$qZZplq|P_V zqvBbXVtkRkGgR}8xq~02bkA|rZGVw4Ajyi!seFvK@C~bKw^L|TWVh`^1HoF_G z+xxRO1vjm;-;wezPhy{F285iFfTmXc?kYUdY(7^Zpc>#d3sevlDmyZ-z?`mY+D}xd zm}LmtDYYQmCtJS&f|meG@$Ru~y+KR}BjH#Vt@wUWeJ*%5DwZabHJAZOen)Q0) zR_oNqw$PF*waTHjoPWaNFr?LqV1Z)pIQFwpS7DOV7?v#PVq0@o*-O+DdPWW|!hU6L0OV6S%(4yk5e1hdYo;B zaBbnVuVb{@AVA>k**gQx73+g4gTSS#09lDZT*aWZNwnSR#g;jVZ<*v&5vGJPSJ zX+Zj%gQiXh9vJ4Y|1tPHCGz4x5gA#h5RFU@oCw$vccQ|s6EUhd3)F1sIohDYVkQN> z2XUPL$2va{zx1}fGO}t{zhV04SwLEarg-lgfiJP|cGf@vIA(*|xO{DlPnbbIl^3>7 z>e{+yA31MlCHuZoktd+KKKq@n1f>XV&YNWRfTg)FQKwCrTxA_DNTT$`ZLF;+#w0&+Ao?G%M~&=n0Cp#G&xtBk-ZA?Cox@u zs|8-!gfEwq_MTLKc?h_8`I_9%zR;TkH64Cc5uDQzXo?%PxGgoIRjgC|B-|F*15_xlfy6Wi3S|BDYcjPxvNH5u)tM5yrLH-8~EucK2wqSGQ0lLHe z`X^;slUyQLbY)TyB;sThksTsH$Vj!%5R@#KYWFGt9KSF{6kof>uYQBpN3ezcyu}IA zQyZ(GS2_dJ28g7;f5Wf4y%petTnw>Cm~bd-M-eX35}ApYYEMu>*HMQk*@mMPIT6!( z9ZFv^`dx@!GWF3CS2I*jrFc(8{_x-VmmsOEfClq2OXR{CH?P3v-iIPZffdI^j{}VA zUDDEGTa=2Ql6)lczwjYG1m5&3q}6!O(W1V2)R=79!D$a6zX!OM@`QmvBeriQIs@(~ zL|=04q_K;tCZ_iSvTf%6Arb;pH4?H3^1if(ovlu>=7B1*EAO9v&|$eG8e~a1(Ls{} z3N8pLFF*w<4XWTXP5o;gu5!~J1J~H1Z)V|_eXP?&HH*Q+N^25I^+DH{wOOgK5r95o z5?JMwav=O3%z>7&i(DWs=?vf(?YGC&k@sIP3wUz=P`O8`;2b0U4zN`+%pt9-wDl!J z<YX+7NE$56z9 z;~tWrFGk#AZaM#b40BGj8N_$~D1ir%9yHD%<%?aXT(Df4us(g^_qFl98bQYA3OE#` zv_*yF2)XusSGD)?-v>(tR{6vjLBmrj&*4!dyt=lxt1!H0Y?-zA$a~X-txfj$?(HztmVYMar0mv`#KI5S-?&-jO>qSjzG%dnQKzM&z@pLQ5;K8^)C1p_~}Rj?Yu*yD<5-s6S9YKu%e z5?6V9$~~Adj)?meR#C(8b>}l1WkD)nl!f1m$gggQi64EeY^8D&9B&_N^_akf0k(KK zAkDSe#JG9>r2vXT5;;{Ce`7yZ^pftt*R-0T#QmzDX=o}!K>IOLZvd)b&=vJ0dAr^F z?fu$`%fM^!k-d4AgF+6#u$J9h4O(IS`W#(7`{{>a`^>#-M$-xb;}}Ftqy7NcmuAH~ zQS*Lu)%TczpUFUDY3sYJc>-`nC{9yJBH+dSM6V68bLV!RreAB6VB6(CsBFCDx~&wg zP_ghbTd1p49$}hvz88|k?ooH`qNTuTM_0~U={yl3EqhEm*yb)mxSac7l6>3ruKr|0 z9AKDfe9=-*H}5~axu5WZJlxg*V`fUC>p=^aQTswTB!jc5$4xA*L5O9>v1>qRmBNIp z2m5DoHhIJ)JxmbWHjta_3Y$S+k(Tu#DOxfdbV%9$ib)Sl@TW*1klDjC*I*WNrL|tL zK|by@`LcWM0!DI1XHS9sUWZWdkcee(%H+oryvOeN*MHaItIc`lVzFPUi#FrZt>?M`grzoW$pO8*0ZZ9Rh>9%G zj3nC`ufp3^+ixf+3$L&qfPpYr(@Ai%22NsN3*A!X{JdeP`59L-Zp(d|JtZ54`PdS~ zaxT-{5EC&P-lW#)?^oAvLS+Qe$4?$V(j8Fyoh1@>B%Gj6WYz5vY~7IU!f((4I?A%> zEQcsUG>EY)SU$X6qN%it;=VsCyaM*L&9!`=uAQxnLU}V<;a(!h5vGFUi#F&Z$qi1| z$bGukhpWf;{oZN4EA$V(u640tSZR|Vw3VEb5-lv%tgHG*i{P8pcV5-PwCklj{1q%K znvB;TOSNzqk5=6WgrSuWcjANq{`S=Jld=?aPfGgs9^E(L)sayecHsg9NeCNs6ZRA#5%A>iCOa(m z&f|9cGV6%)!`0`kN)EistYJ^w&v=hBsy&v34z9U6qh^0S8#XpMVIZt%@-cf}zc>jy za`&gyaVdTMtG)a($ZccjxUAyk#iPVV_2PRYDQc+ydM_6g$zOoH>6Ai9z=GEhZSJbz zQM6=uJ8etp@~~UZp|?gmD*v8x5enS87n-d;T-$JS_;4t@Y#g$!yFuxSuJ-wdLV7af zQ_Q+G%=#Zg%h{mJa}$H$n{0bsi{{(5U=-vq;!#EswZh>s9+44eN8o>TfT0aPHKX-m z3Sa<_5r>uE5D?n0-Z>ntL9MM%gJ}{M$nDLQ7F_~r!|9dsU{u{tRq9lmg%k>_?3L*% z%8IW_xsrTosimDxq?G2&EhC0W+gcRLrfj8+qL4+Nz82MxP$sSgzf6^25J`4+qZt`z z!=xdWv|EkK8dJPF*5SUtW?fXhy=vqa)MAAZ-iClg=h4IN;bZ7$QdRM=Y=3m(uF}IW z#oniZu@VP52*8?DjRir>LvYqSL@JC+HG2!h&p5z{p(af<(2=`>UB{TMTYj9Cbd~Vr@Pc z(M9RH%jRkNbB!jPeJU8b>Jr{nYk34v3cQRHL0$ApX;dkK0XBlEi7qOK-y8# zZl|3|KYqdg?6zUJf?A}J9?`Vf1z_4VY%XTv;lQ$bHdN;8;+O>l8t8;c6oS1Z|do8Q$$H(xW)Qg|akwTIV2 zn1>zq5#<^p+vC~2FVIK9uHkmp-B%l#<(T=YDsmLUGyE!}d1n^-TI-@g3eiEuC} z=BVn;);a5((;LLkOeSPG(&Zz3!Hctlk zJDtH#H1u6)>Bxe0$wv}A07&v1Jj~iylrt9~ z0hN<)P_8tmJc>)(`k}T%CI#rOB4zYLTF3RwUg0L^Zh0`tF^n@?z=F6y&|;ZSwNG5< ztlgsarM9gP!q#;GlT!|CYW}JmRKEk+W^t;ti^e8bHmM!bZdl7(Us`;sbAhnu2`JUF ze|2~&y%x~&O`iFW#zb%j99*s?P&sA$%{-p9Z;cUS%9=@o-h4gfYO?a*3Fkfa1}Wpr z<;H`8CW+01pv5`?3a9oWhj|hw%V9OgJcS+CR~?3n#tT;a;y6y*LH8P&Vs_|fR4@D> zB?wkP5A-MmG1@aAXn9wfdT2X;XMl#XAiB&T&r;^S9J&LOR+Qdv! zF{k=7R>b7LWQ#tcI(s^BYkP3Q?k|xwekzF;gi(Gerkq)z=l6`fV^bd3bWx`MAJX^!W{6cv`A*WyIp=1J1zdpS2 z({=g_K$x+i73JrxlRELsMeSL1RD@?(5zy1NeC%U(u+*k*sV~!F&hY#xR3p+_j4}IF z{Qh*WP~RHJ5VBWU;oMNC{YJY1F3(4oSzcWi?8l7-N?;00F=N+~d0&bI**6vTx(E_c z+r(Cg@VoLiV3 zG(>PLGap>eMch>Gr&`n<{aV%D{DD5oH;Muc#Qr|3fn1?n;Su!}`^Z_TK#Q|q3CKZ5fWO@pkd2=o(VFuvkxj&u(H_gO z0~gaYJ_R3ZweK&y;2$UTIhA|1+Bq!EQFDqRir}A(G20|mf7c8GqFQgIyLqm|dux`D z?Cr_eo~54@$Eq|}`W|(_1DfznZ-7ur;WYw-PYWWQijk)rX8}h!ZqJe_Ajh=55>4JfkJ`a%}FC&kt!R}Knv?P2< zP@E~uWI9B|D>hU5Fio2`jW#76AD5lEWkp3>JG^tw;CHz@OKn->m=dc)#OA3TA;PXG z1*|#uDo%GYQd4zSKxcKOwzI8-BH+pTNe584#No!sC6J9sGsk=1QaTWyrU!1MV@6(` zvS0OKY8t4|%S+APm=IY8!&nqxt8%M%zrL%%_WfaH1W-#L#1$4u9hcB}pV`VFSc?59 zLrQH2!#?$n@tSJ44l#ZB`Sj=i68zOvC~|Y!_4-19KsuO7(<0769=%{T=e_bhrfh?` zY{J^dp~yKy*rn#-;iE4z%EFnrr4Khhst#ofGw;vcrl#K-E01Pc(Xa6BJu;c))mdGw z?NTYJR{x)@M3CLXFwgw-b%FVv9{~ejjczQ|@_?+yJHu=`iC?k@v+JmtZyv`Lu4zqI zD!J_yh~8P=hovy>2%7u|xuIdcBcy3m5Ij@$Y-kHDz>Rh(`C3pM?c3zoF$T)m#Xw4A zmK6Dp{F%4950l*0W3Fyp38D&R!uQit!~+>=hg*f6#y2PpiCqQ5e`eL^kZtyWut>-w z`v-DM-dB@l2$?&db~JneGY`mqFTL&tl(|vynp#Lh-jxx1xNZ?h`I7jt4l@rY%ib*s z%V?byNA=|F+l7i*&j0PA0No6_sBJ2f#w$RL2&D>c7A!8Ru1v^nzbJW7Jxv?~A40an z@6gcCFP$s<)5JZ#?30c8x-DSTy7l(xQERXs)4Afl(cyMnoYC+^YUmRO|85E1F4Xw!|QK+6*3P5v*293#kx^XJaI)1VROrBAW5v)Li2j+%6GrnsK0d>m^3oe>o zBK74>m8z-uW!O4m?51?QGTDQNH$}g7X?!ES>c%yP1o#l>(&DhT#7Pwgbf(Nac=gyq zug_3rWA=j~ysBZ*#&LdHWBZJ)izlEEbn>8^+m`ip9!klJZob!VR+a1Zod&eoB4&;^ zfY8f!!8XlRtaz!yjV{jo);#3fHc|$ay$T~XDoF~!015&&i_gR}#C*0U6bh(jg$YZW;yO5wOMBctwk@1F-8DtW&0{CZZg z3C0k6O4Tkm(7O?5#%t9vTGFN;SEX&IVh#_oQj4Yqw-kYjhbaE9nx!yxsIi!NeCBq_ zTg~-Bz&EI(KsitnG`N8z<<&%aTJkO~)g~i!^%ySA*ml8^$N5@oR;lwjgNZcl^q zW9+BlSo$VW_)d4OQht_C@RT!65`d&&UPJW2JP=P8`Ydb8V#=B=W;`c)IP3^_^!j`( z1!vYyx;!>6`Q+6w(F0k$(y$LJR4=f9@zR+9O6B7D-?~*9&QZR)z~;7*&HE)7s{CqE zXu&&WY^R+&ZVGU_DuV^a#L>bDY)G4rwh<_!(JH ze%&Kv{0rn$P?$k~!}=N11NqBlS4Vasx!SXj%r@^+D0eEObCF_1CanXsc?}`0$({xC zbF%3p?&oBsz5}ukHaP3Zi6bb1Rdw>Habr=J^k(g0#AToKlG2I-Bi+4ZkV%3_r6vDj zVsqQObn?JGM~B6hCUF*w*b_bIQG8B*FIk{^$$L7qXW%ei45jka7iXFx6(7YOoO#sf_TL4T*!SD@&-15S9E+U37H`6a+buSiaa> zgNNjJd3*0;L_q01Q}a)dc+SKo7HfvfIstc&gfu_7#`|1{Pco|#k&+n#&?R%TvG#Lx zp?pI39`Wkb+r@X(LjE)F<=E@b?wy8g`&n^eR>t0vgKqCSWiz`4Yzy(UGD~>DcYw1v zEB^7m{~J4tgT$8L|6K9_;EsER6HtXSyl_7AS|K`^29S@ z{uVs}LazMs8?VtnikFW*{#JtC1m2AiNwg`!e9@LW1e6kx%4Y3&+&O%wqFmBMSBpED zaQ!a|(|jtpI`*0$$jOs#K75no-5%G@`j$9Borj?MdLgc~a5<)dJ)(nYgGtS)D{>CG z#Y@d3c-TO?MS=@O2=U5@Q);LX=gP~(C>hiPfXSt}<^gwvX;yT)lcQOXquj zsM($$`43J|ZQ{oMLs~E@lfDfttK6w-ehSEyB1|E;dPJ3A2h{Si{s~nemOb{Ts7Ktr zb^27=4W%|E<3r(o5ILanCBkLe9dNq=2vCwlVRu4*W5WywH6uefms=o%opBN)RVkq0 z>UU4ZsH;c>nC9quup)mi(oxkjN~e2!kebSI@ah<)O)u;c?{l#Msq&0GF`z04A;PFg z-&a6_!qGS5zU((3yAXyfF&Q+2X9AR6NzlNs(0hpZ(r5$kwnT~~>Xl$ZinAJWeC4l? zxa<4S#O43`NoP|lKm-J*)b!?J-=asaY8ox`2_PD6a|!pE7Uum8EECKqr}Nbz!6v+$-T z(8-h$jr_7Iz4ypL!SpEg5p9%_*h~U^Fpwf@+*EYS8$bmzdJm?6x6@Cr;;|RN*qRc| zuF#UQmYnxvaJj0t?T-wqF`5W>!PMe(MhaCGTH-T!?eUR6!7xRgjeLg_300U-Y1e7l#O z;FTEMo0Kuh53cyg8)U^>GX5v`0S?L=Brth!AVv^p#_k{N;(lY9Zcm0j5kwx4sntw6 z9rNM~&cJn@<)KSR3JR6R$GjqoN{D8bG(;kG6BENh*K0 z!TC7^C?aZ!O+52WriXuXxN3p39UF&@V*1U zABhvuwSS)w%8$5KK0Z__TbvRIzjiy=Sx_V#XBpXQ{zv0IJ^n|M(`wtzi7Ap!9dZAJ z0oTE_BqFr3E3t%sg$&C#&i2<>{?bk+y zHSb6-BG!y9q>4$H2L4S42Db{x=#RL&=Q35lHNu9O+_dLMV3XevNi~ql0QzNzvS)z{ zX?;DfFcm&`7EONRsBLPa3(!mDLa*fQs2=D($h`P-z^@MM$r7zr3Ra*#9yJiuVd`n3 za!1KYhGe8i^(Oq<*&-e>`aidVj#;3a;kc3TOKIcTO-sQ`ff-*;@;g0{?k>mw^a1+s z0dmSNiXcMgNdZOI7I|oczWdWmpg>7*AgehvVka){MOU4xx}XCkOfo?sQX&kU&e>y{ zSc7o#B}$4~+1bgXwwZPVEe;r{dn^s*#%^yi2)yR(C=@bu2Jy>4)cUEnSY#=_T}_89 zzka1RKn-c(Q#M0fXk~pi^Y9{@zUt--t$%#GtM7hBz*R%`w<;pk~*5R_jJx%eCxDInRgb^SCfnoX)O95haIrweA{ESarV2g*coY;}`4MRH2bTvJU^9NZp8td1AOODNvvdX4^u zWWe$dc0YB;k`2<^^Qb{q=#llLiNde2|NCcL;T5;5gEv+}Js#IXBDhR9?!XgT)fbb8QLjcfUU>)HvJx=@H$Bmw#YWW6oj*E0 z$_g%fm7@`mZF3t1H{cA#e)Rp9_hw6wUzeFi!)`z`AEj39xFKx`?&~SP-T(a+1OLZw zSU~C>1sQLww5jn*Er8zx!Q+82@UVLBVJ&0nW(~fPh}{ Date: Sun, 7 Jul 2024 00:51:44 -0400 Subject: [PATCH 3/3] Delete unnecessary common file. --- glue_ar/common/common.py | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 glue_ar/common/common.py diff --git a/glue_ar/common/common.py b/glue_ar/common/common.py deleted file mode 100644 index 8cc06f8..0000000 --- a/glue_ar/common/common.py +++ /dev/null @@ -1,29 +0,0 @@ -import pyvista as pv - - -def layers_info(info_creator, viewer_state, layer_states=None): - info = {} - if layer_states is None: - layer_states = list((layer for layer in viewer_state.layers if layer.visible)) - - for layer_state in layer_states: - info[layer_state.layer.uuid] = info_creator(viewer_state, layer_state) - - return info - - -def create_plotter(adder, info_creator, viewer_state, layer_states=None): - plotter = pv.Plotter() - - info = layers_info(info_creator, viewer_state, layer_states) - for layer_info in info.values(): - data = layer_info.pop("data") - try: - adder(plotter, data, **layer_info) - - # The only error so far I've seen is a ValueError when using Plotter.add_mesh if - # the mesh is empty (n_arrays = 0). We can be more explicit once our code is more definite - except Exception: - pass - - return plotter