diff --git a/glue_ar/__init__.py b/glue_ar/__init__.py index 40e1aa0..51b9a2c 100644 --- a/glue_ar/__init__.py +++ b/glue_ar/__init__.py @@ -1,3 +1,5 @@ +from glue_ar.export_scatter import * +from glue_ar.export_volume import * from glue_ar.tools import * def setup(): diff --git a/glue_ar/export.py b/glue_ar/export.py index 8ace078..d891fe0 100644 --- a/glue_ar/export.py +++ b/glue_ar/export.py @@ -45,7 +45,7 @@ def export_gl(plotter, filepath, with_alpha=True): if glb or with_alpha: gl = GLTF.load(gltf_path) - if with_alpha: + if with_alpha and gl.model.materials is not None: for material in gl.model.materials: material.alphaMode = "BLEND" export_gl_by_extension(gl, filepath) diff --git a/glue_ar/export_dialog.py b/glue_ar/export_dialog.py new file mode 100644 index 0000000..2d2cd3c --- /dev/null +++ b/glue_ar/export_dialog.py @@ -0,0 +1,126 @@ +import os + +from echo import 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 qtpy.QtGui import QDoubleValidator, QIntValidator + + +from qtpy.QtWidgets import QCheckBox, QDialog, QHBoxLayout, QLabel, QLineEdit +from qtpy.QtGui import QIntValidator, QDoubleValidator + + +__all__ = ['ar_layer_export', '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() + + def __init__(self, viewer_state): + + super(ARExportDialogState, self).__init__() + + self.filetype_helper = ComboHelper(self, 'filetype') + self.filetype_helper.choices = ['glTF', 'OBJ'] + + self.layers = [state for state in viewer_state.layers if state.visible] + 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_state=None): + + super(ARExportDialog, self).__init__(parent=parent) + + self.viewer_state = viewer_state + self.state = ARExportDialogState(self.viewer_state) + self.ui = load_ui('export_dialog.ui', self, directory=os.path.dirname(__file__)) + + layers = [state for state in self.viewer_state.layers if state.visible] + self.state_dictionary = { + layer.layer.label: ar_layer_export.members[type(layer)]() + for layer in layers + } + + self._connections = autoconnect_callbacks_to_qt(self.state, self.ui) + self._layer_connections = [] + + self.ui.button_cancel.clicked.connect(self.reject) + self.ui.button_ok.clicked.connect(self.accept) + + self.state.add_callback('layer', self._update_layer_ui) + + self._update_layer_ui(self.state.layer) + + def _widgets_for_property(self, instance, property, display_name): + value = getattr(instance, property) + t = type(value) + if t is bool: + widget = QCheckBox() + widget.setChecked(value) + widget.setText(display_name) + self._layer_connections.append(connect_checkable_button(instance, property, widget)) + return [widget] + elif t in [int, float]: + label = QLabel() + prompt = f"{display_name}:" + label.setText(prompt) + widget = QLineEdit() + validator = QIntValidator() if t is int else QDoubleValidator() + widget.setText(str(value)) + widget.setValidator(validator) + self._layer_connections.append(connect_float_text(instance, property, widget)) + return [label, widget] + + def _clear_layout(self, layout): + if layout is not None: + while layout.count(): + item = layout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.deleteLater() + else: + self._clear_layout(item.layout()) + + def _clear_layer_layout(self): + self._clear_layout(self.ui.layer_layout) + + def _update_layer_ui(self, layer): + self._clear_layer_layout() + self._layer_connections = [] + state = self.state_dictionary[layer] + for property in state.callback_properties(): + row = QHBoxLayout() + name = display_name(property) + widgets = self._widgets_for_property(state, property, name) + for widget in widgets: + row.addWidget(widget) + self.ui.layer_layout.addRow(row) diff --git a/glue_ar/export_volume.ui b/glue_ar/export_dialog.ui similarity index 69% rename from glue_ar/export_volume.ui rename to glue_ar/export_dialog.ui index dbd6063..5862f27 100644 --- a/glue_ar/export_volume.ui +++ b/glue_ar/export_dialog.ui @@ -14,16 +14,6 @@ Export 3D Volume - - - - Select the export filetype - - - - - - @@ -31,38 +21,6 @@ - - - - - - - - - - Use Gaussian filter - - - - - - - - - - Smoothing iterations - - - - - - - - - - - - @@ -96,6 +54,22 @@ + + + + + + + + + + Select the export filetype + + + + + + diff --git a/glue_ar/export_scatter.py b/glue_ar/export_scatter.py index 10d6519..7ab9ec7 100644 --- a/glue_ar/export_scatter.py +++ b/glue_ar/export_scatter.py @@ -1,77 +1,13 @@ -import os - -from qtpy.QtWidgets import QDialog, QListWidgetItem - +from echo import CallbackProperty from glue.core.state_objects import State -from glue.core.data_combo_helper import ComboHelper -from glue_qt.utils import load_ui +from glue_ar.export_dialog import ar_layer_export +from glue_vispy_viewers.scatter.layer_state import ScatterLayerState -from echo import CallbackProperty, SelectionCallbackProperty -from echo.qt import autoconnect_callbacks_to_qt +__all__ = ["ARScatterExportOptions"] -__all__ = ["ExportScatterDialog"] -# Note that this class only holds the state that is -# currently displayed in the dialog. In particular, -# this means that `theta_resolution` and `phi_resolution` -# represent the resolutions for `layer` -class ExportScatterDialogState(State): +@ar_layer_export(ScatterLayerState) +class ARScatterExportOptions(State): - filetype = SelectionCallbackProperty() - layer = SelectionCallbackProperty() theta_resolution = CallbackProperty(8) phi_resolution = CallbackProperty(8) - - def __init__(self, viewer_state): - - super(ExportScatterDialogState, self).__init__() - - self.filetype_helper = ComboHelper(self, 'filetype') - self.filetype_helper.choices = ['glTF', 'OBJ'] - - self.layers = [state for state in viewer_state.layers if state.visible] - self.layer_helper = ComboHelper(self, 'layer') - self.layer_helper.choices = [state.layer.label for state in self.layers] - - -class ExportScatterDialog(QDialog): - - def __init__(self, parent=None, viewer_state=None): - - super(ExportScatterDialog, self).__init__(parent=parent) - - self.viewer_state = viewer_state - self.state = ExportScatterDialogState(self.viewer_state) - self.ui = load_ui('export_scatter.ui', self, directory=os.path.dirname(__file__)) - - layers = [state for state in self.viewer_state.layers if state.visible] - self.info_dictionary = { - layer.layer.label: { - "theta_resolution": 8, - "phi_resolution": 8 - } for layer in layers - } - - for layer in layers: - item = QListWidgetItem(layer.layer.label) - self.ui.listsel_layer.addItem(item) - - self._connections = autoconnect_callbacks_to_qt(self.state, self.ui) - - self.ui.button_cancel.clicked.connect(self.reject) - self.ui.button_ok.clicked.connect(self.accept) - - self.state.add_callback('theta_resolution', self._on_theta_resolution_change) - self.state.add_callback('phi_resolution', self._on_phi_resolution_change) - self.state.add_callback('layer', self._on_layer_change) - - def _on_theta_resolution_change(self, resolution): - self.info_dictionary[self.state.layer]["theta_resolution"] = int(resolution) - - def _on_phi_resolution_change(self, resolution): - self.info_dictionary[self.state.layer]["phi_resolution"] = int(resolution) - - def _on_layer_change(self, layer): - self.state.theta_resolution = self.info_dictionary[layer]["theta_resolution"] - self.state.phi_resolution = self.info_dictionary[layer]["phi_resolution"] - diff --git a/glue_ar/export_scatter.ui b/glue_ar/export_scatter.ui deleted file mode 100644 index 48b1d95..0000000 --- a/glue_ar/export_scatter.ui +++ /dev/null @@ -1,116 +0,0 @@ - - - Dialog - - - - 0 - 0 - 327 - 416 - - - - Export 3D Scatter - - - - - - Select the export filetype - - - - - - - - - - Select the export settings for each layer - - - - - - - - - - - 0 - 0 - - - - <html><head/><body>The resolution values affect how many points are used to draw each sphere. Higher values mean better resolution, but a larger exported file. The total number of points per sphere is <div>2 + (phi_resolution - 2) * theta_resolution</div></body></html> - - - true - - - - - - - - - - Theta resolution: - - - - - - - Phi resolution: - - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Cancel - - - - - - - Export - - - - - - - - - - - diff --git a/glue_ar/export_volume.py b/glue_ar/export_volume.py index 9a40287..a3fc481 100644 --- a/glue_ar/export_volume.py +++ b/glue_ar/export_volume.py @@ -1,77 +1,13 @@ -import os -import typing -from PyQt6 import QtCore -from PyQt6.QtWidgets import QWidget -from echo import CallbackProperty, SelectionCallbackProperty -from echo.qt import autoconnect_callbacks_to_qt +from echo import CallbackProperty +from glue.core.state_objects import State +from glue_ar.export_dialog import ar_layer_export +from glue_vispy_viewers.volume.layer_state import VolumeLayerState -from qtpy.QtWidgets import QDialog, QListWidgetItem +__all__ = ["ARVolumeExportOptions"] -from glue.core.state_objects import State -from glue.core.data_combo_helper import ComboHelper -from glue_qt.utils import load_ui -__all__ = ["ExportVolumeDialog"] +@ar_layer_export(VolumeLayerState) +class ARVolumeExportOptions(State): -# Note that this class only holds the state that is -# currently displayed in the dialog. In particular, -# this means that `gaussian_filter` and `smoothing_iterations` -# represent the values for `layer` -class ExportVolumeDialogState(State): - - filetype = SelectionCallbackProperty() - layer = SelectionCallbackProperty() - use_gaussian_filter = CallbackProperty() - smoothing_iterations = CallbackProperty() - - def __init__(self, viewer_state): - super(ExportVolumeDialogState, self).__init__() - - self.filetype_helper = ComboHelper(self, 'filetype') - self.filetype_helper.choices = ['glTF', 'OBJ'] - - self.layers = [state for state in viewer_state.layers if state.visible] - self.layer_helper = ComboHelper(self, 'layer') - self.layer_helper.choices = [state.layer.label for state in self.layers] - - -class ExportVolumeDialog(QDialog): - - def __init__(self, parent=None, viewer_state=None): - - super(ExportVolumeDialog, self).__init__(parent=parent) - - self.viewer_state = viewer_state - self.state = ExportVolumeDialogState(self.viewer_state) - self.ui = load_ui('export_volume.ui', self, directory=os.path.dirname(__file__)) - - layers = [state for state in self.viewer_state.layers if state.visible] - self.info_dictionary = { - layer.layer.label: { - "use_gaussian_filter": False, - "smoothing_iterations": 0 - } for layer in layers - } - - for layer in layers: - item = QListWidgetItem(layer.layer.label) - self.ui.listsel_layer.addItem(item) - - self._connections = autoconnect_callbacks_to_qt(self.state, self.ui) - - self.ui.button_cancel.clicked.connect(self.reject) - self.ui.button_ok.clicked.connect(self.accept) - - self.state.add_callback('use_gaussian_filter', self._on_gaussian_filter_change) - self.state.add_callback('smoothing_iterations', self._on_smoothing_iterations_change) - self.state.add_callback('layer', self._on_layer_change) - - def _on_gaussian_filter_change(self, filter): - self.info_dictionary[self.state.layer]["use_gaussian_filter"] = filter - - def _on_smoothing_iterations_change(self, iterations): - self.info_dictionary[self.state.layer]["smoothing_iterations"] = int(iterations) - - def _on_layer_change(self, layer): - self.state.use_gaussian_filter = self.info_dictionary[layer]["use_gaussian_filter"] - self.state.smoothing_iterations = self.info_dictionary[layer]["smoothing_iterations"] + use_gaussian_filter = CallbackProperty(False) + smoothing_iterations = CallbackProperty(0) diff --git a/glue_ar/scatter.py b/glue_ar/scatter.py index 63d61b5..92c71d9 100644 --- a/glue_ar/scatter.py +++ b/glue_ar/scatter.py @@ -35,8 +35,9 @@ def scatter_layer_as_glyphs(viewer_state, layer_state, glyph): def scatter_layer_as_multiblock(viewer_state, layer_state, theta_resolution=8, - phi_resolution=8): - data = xyz_for_layer(viewer_state, layer_state, scaled=True) + phi_resolution=8, + scaled=True): + data = xyz_for_layer(viewer_state, layer_state, scaled=scaled) spheres = [pv.Sphere(center=p, radius=layer_state.size_scaling * layer_state.size / 600, phi_resolution=phi_resolution, theta_resolution=theta_resolution) for p in data] blocks = pv.MultiBlock(spheres) geometry = blocks.extract_geometry() @@ -54,7 +55,12 @@ def scatter_layer_as_multiblock(viewer_state, layer_state, point_cmap_values = [y for x in cmap_values for y in (x,) * sphere_points] # geometry.cell_data["colors"] = cell_cmap_values geometry.point_data["colors"] = point_cmap_values - info["cmap"] = layer_state.cmap.name # This assumes that we're using a matplotlib colormap - info["clim"] = [layer_state.cmap_vmin, layer_state.cmap_vmax] + cmap = layer_state.cmap.name # This assumes that we're using a matplotlib colormap + clim = [layer_state.cmap_vmin, layer_state.cmap_vmax] + if clim[0] > clim[1]: + clim = [clim[1], clim[0]] + cmap = f"{cmap}_r" + info["cmap"] = cmap + info["clim"] = clim info["scalars"] = "colors" return info diff --git a/glue_ar/tools.py b/glue_ar/tools.py index 6645055..966bafd 100644 --- a/glue_ar/tools.py +++ b/glue_ar/tools.py @@ -1,5 +1,6 @@ import os from os.path import join, split, splitext +from glue_vispy_viewers.volume.layer_state import VolumeLayerState import pyvista as pv @@ -8,9 +9,8 @@ from glue.config import viewer_tool from glue.viewers.common.tool import Tool +from glue_ar.export_dialog import ARExportDialog -from glue_ar.export_scatter import ExportScatterDialog -from glue_ar.export_volume import ExportVolumeDialog from glue_ar.scatter import scatter_layer_as_multiblock from glue_ar.export import export_gl, export_modelviewer from glue_ar.volume import bounds_3d, meshes_for_volume_layer @@ -32,7 +32,7 @@ class GLScatterExportTool(Tool): def activate(self): - dialog = ExportScatterDialog(parent=self.viewer, viewer_state=self.viewer.state) + dialog = ARExportDialog(parent=self.viewer, viewer_state=self.viewer.state) result = dialog.exec_() if result == QDialog.Rejected: return @@ -44,7 +44,7 @@ def activate(self): plotter = pv.Plotter() layer_states = [layer.state for layer in self.viewer.layers if layer.enabled and layer.state.visible] for layer_state in layer_states: - layer_info = dialog.info_dictionary[layer_state.layer.label] + layer_info = dialog.state_dictionary[layer_state.layer.label].as_dict() mesh_info = scatter_layer_as_multiblock(self.viewer.state, layer_state, **layer_info) data = mesh_info.pop("data") plotter.add_mesh(data, **mesh_info) @@ -68,7 +68,7 @@ class GLVolumeExportTool(Tool): def activate(self): - dialog = ExportVolumeDialog(parent=self.viewer, viewer_state=self.viewer.state) + dialog = ARExportDialog(parent=self.viewer, viewer_state=self.viewer.state) result = dialog.exec_() if result == QDialog.Rejected: return @@ -82,11 +82,14 @@ def activate(self): bounds = bounds_3d(self.viewer.state) frbs = {} for layer_state in layer_states: - layer_info = dialog.info_dictionary[layer_state.layer.label] - mesh_info = meshes_for_volume_layer(self.viewer.state, layer_state, - bounds=bounds, - precomputed_frbs=frbs, - **layer_info) + layer_info = dialog.state_dictionary[layer_state.layer.label].as_dict() + if isinstance(layer_state, VolumeLayerState): + mesh_info = meshes_for_volume_layer(self.viewer.state, layer_state, + bounds=bounds, + precomputed_frbs=frbs, + **layer_info) + else: + mesh_info = scatter_layer_as_multiblock(self.viewer.state, layer_state, **layer_info, scaled=False) data = mesh_info.pop("data") plotter.add_mesh(data, **mesh_info)