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)