Skip to content

Commit

Permalink
Merge pull request #28 from Carifio24/jupyter
Browse files Browse the repository at this point in the history
Add export tool to Jupyter VisPy viewers
  • Loading branch information
Carifio24 authored Jul 4, 2024
2 parents 95c2c2b + e00c5d0 commit 8bc6b9c
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 68 deletions.
36 changes: 30 additions & 6 deletions glue_ar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,44 @@
from glue_ar.tools import * # noqa


def setup():

from glue_vispy_viewers.scatter.qt.scatter_viewer import VispyScatterViewer
VispyScatterViewer.tools += ["ar"]
def setup_qt():
try:
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"]
VispyScatterViewer.subtools = {
**VispyScatterViewer.subtools,
"save": VispyScatterViewer.subtools["save"] + ["save:ar"]
}
VispyScatterViewer.subtools["ar"] = ["ar:qr"]

from glue_vispy_viewers.volume.qt.volume_viewer import VispyVolumeViewer
VispyVolumeViewer.tools += ["ar"]
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"]


def setup_jupyter():
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"]
JupyterVispyVolumeViewer.tools = [t for t in JupyterVispyVolumeViewer.tools] + ["save:ar_jupyter"]


def setup():
try:
setup_qt()
except ImportError:
pass

try:
setup_jupyter()
except ImportError:
pass
57 changes: 56 additions & 1 deletion glue_ar/export.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from os import remove
from os.path import abspath, dirname, join, splitext
from os.path import abspath, dirname, join, split, splitext
from subprocess import run

import pyvista as pv
from gltflib.gltf import GLTF

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.utils import bounds_3d_from_layers, xyz_bounds
from glue_ar.volume import bounds_3d, meshes_for_volume_layer


GLTF_PIPELINE_FILEPATH = join(dirname(abspath(__file__)), "js",
"node_modules", "gltf-pipeline",
"bin", "gltf-pipeline.js")
Expand Down Expand Up @@ -169,3 +177,50 @@ def export_modelviewer(output_path, gltf_path, alt_text):

with open(output_path, 'w') as f:
f.write(html)


def create_plotter(viewer, state_dictionary):
plotter = pv.Plotter()
layer_states = [layer.state for layer in viewer.layers if layer.enabled and layer.state.visible]
scatter_viewer = isinstance(viewer.state, Vispy3DScatterViewerState)
if scatter_viewer:
bounds = xyz_bounds(viewer.state)
elif viewer.state.clip_data:
bounds = bounds_3d(viewer.state)
else:
bounds = bounds_3d_from_layers(viewer.state, layer_states)
frbs = {}
for layer_state in layer_states:
layer_info = state_dictionary.get(layer_state.layer.label, {})
if layer_info:
layer_info = layer_info.as_dict()
if isinstance(layer_state, VolumeLayerState):
meshes = meshes_for_volume_layer(viewer.state, layer_state,
bounds=bounds,
precomputed_frbs=frbs,
**layer_info)
else:
meshes = scatter_layer_as_multiblock(viewer.state, layer_state,
scaled=scatter_viewer,
clip_to_bounds=viewer.state.clip_data,
**layer_info)
for mesh_info in meshes:
mesh = mesh_info.pop("mesh")
if len(mesh.points) > 0:
plotter.add_mesh(mesh, **mesh_info)

return plotter


def export_to_ar(viewer, filepath, state_dict, compress=True):
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_modelviewer(html_path, base, viewer.state.title)
else:
plotter.export_obj(filepath)
2 changes: 2 additions & 0 deletions glue_ar/tools/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .jupyter import * # noqa
from .qt import * # noqa
Binary file added glue_ar/tools/ar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions glue_ar/tools/ar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions glue_ar/tools/jupyter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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

import ipyvuetify as v # noqa
from ipywidgets import HBox, Layout # noqa
from IPython.display import display # noqa
from ipyfilechooser import FileChooser

__all__ = ["JupyterARExportTool"]


AR_ICON = os.path.abspath(os.path.join(os.path.dirname(__file__), "ar"))


@viewer_tool
class JupyterARExportTool(Tool):
icon = AR_ICON
tool_id = "save:ar_jupyter"
action_text = "Export 3D file"
tool_tip = "Export the current view to a 3D file"

def activate(self):
file_chooser = FileChooser(getcwd())
ok_btn = v.Btn(color='success', disabled=True, children=['Ok'])
close_btn = v.Btn(color='error', children=['Close'])
dialog = v.Dialog(
width='500',
persistent=True,
children=[
v.Card(children=[
v.CardTitle(primary_title=True,
children=["Select output filepath"]),
file_chooser,
HBox(children=[ok_btn, close_btn],
layout=Layout(justify_content='flex-end', grid_gap='5px'))
], layout=Layout(padding='8px'))
]
)

def on_ok_click(button, event, data):
self.maybe_save_figure(file_chooser.selected)

def on_close_click(button, event, data):
self.viewer.output_widget.clear_output()

def on_selected_change(chooser):
ok_btn.disabled = not bool(chooser.selected_filename)

ok_btn.on_event('click', on_ok_click)
close_btn.on_event('click', on_close_click)
file_chooser.register_callback(on_selected_change)

with self.viewer.output_widget:
dialog.v_model = True
display(dialog)

def maybe_save_figure(self, filepath):
if exists(filepath):
yes_btn = v.Btn(color='success', children=["Yes"])
no_btn = v.Btn(color='error', children=["No"])
check_dialog = v.Dialog(
width='500',
persistent=True,
children=[
v.Card(children=[
v.CardText(children=["This filepath already exists. Are you sure you want to overwrite it?"]),
HBox(children=[yes_btn, no_btn], layout=Layout(justify_content='flex-end', grid_gap='5px'))
])
]
)

def on_yes_click(button, event, data):
self.save_figure(filepath)
self.viewer.output_widget.clear_output()

def on_no_click(button, event, data):
check_dialog.v_model = False

yes_btn.on_event('click', on_yes_click)
no_btn.on_event('click', on_no_click)
with self.viewer.output_widget:
check_dialog.v_model = True
display(check_dialog)
else:
self.save_figure(filepath)
self.viewer.output_widget.clear_output()

def save_figure(self, filepath):
export_to_ar(self.viewer, filepath, state_dict={}, compress=True)
63 changes: 5 additions & 58 deletions glue_ar/tools.py → glue_ar/tools/qt.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import os
from os.path import join, split, splitext
from os.path import split, splitext
from tempfile import NamedTemporaryFile
from threading import Thread

from glue_vispy_viewers.scatter.scatter_viewer import Vispy3DScatterViewerState
from glue_vispy_viewers.volume.layer_state import VolumeLayerState

import ngrok
import pyvista as pv

from qtpy import compat
from qtpy.QtWidgets import QDialog
Expand All @@ -20,15 +16,12 @@
from glue_ar.qr import get_local_ip
from glue_ar.qr_dialog import QRDialog

from glue_ar.scatter import scatter_layer_as_multiblock
from glue_ar.export import compress_gl, export_gl, export_modelviewer
from glue_ar.export import create_plotter, export_gl, export_modelviewer, export_to_ar
from glue_ar.exporting_dialog import ExportingDialog
from glue_ar.server import run_ar_server
from glue_ar.utils import bounds_3d_from_layers, xyz_bounds
from glue_ar.volume import bounds_3d, meshes_for_volume_layer


__all__ = ["ARToolMenu", "ARExportTool", "ARLocalQRTool"]
__all__ = ["ARToolMenu", "QtARExportTool", "ARLocalQRTool"]

# This is just some placeholder image that I found online
AR_ICON = os.path.abspath(os.path.join(os.path.dirname(__file__), "ar"))
Expand All @@ -40,39 +33,6 @@
}


def create_plotter(viewer, state_dictionary):
plotter = pv.Plotter()
layer_states = [layer.state for layer in viewer.layers if layer.enabled and layer.state.visible]
scatter_viewer = isinstance(viewer.state, Vispy3DScatterViewerState)
if scatter_viewer:
bounds = xyz_bounds(viewer.state)
elif viewer.state.clip_data:
bounds = bounds_3d(viewer.state)
else:
bounds = bounds_3d_from_layers(viewer.state, layer_states)
frbs = {}
for layer_state in layer_states:
layer_info = state_dictionary.get(layer_state.layer.label, {})
if layer_info:
layer_info = layer_info.as_dict()
if isinstance(layer_state, VolumeLayerState):
meshes = meshes_for_volume_layer(viewer.state, layer_state,
bounds=bounds,
precomputed_frbs=frbs,
**layer_info)
else:
meshes = scatter_layer_as_multiblock(viewer.state, layer_state,
scaled=scatter_viewer,
clip_to_bounds=viewer.state.clip_data,
**layer_info)
for mesh_info in meshes:
mesh = mesh_info.pop("mesh")
if len(mesh.points) > 0:
plotter.add_mesh(mesh, **mesh_info)

return plotter


@viewer_tool
class ARToolMenu(SimpleToolMenu):
tool_id = "ar"
Expand All @@ -81,7 +41,7 @@ class ARToolMenu(SimpleToolMenu):


@viewer_tool
class ARExportTool(Tool):
class QtARExportTool(Tool):
icon = AR_ICON
tool_id = "save:ar"
action_text = "Export 3D file"
Expand All @@ -103,26 +63,13 @@ def activate(self):

_, ext = splitext(export_path)
filetype = _FILETYPE_NAMES.get(ext, None)
worker = Worker(self._export_to_ar, export_path, dialog.state_dictionary, compress=dialog.state.draco)
worker = Worker(export_to_ar, self.viewer, export_path, dialog.state_dictionary, compress=dialog.state.draco)
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_()

def _export_to_ar(self, filepath, state_dict, compress=True):
dir, base = split(filepath)
name, ext = splitext(base)
plotter = create_plotter(self.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_modelviewer(html_path, base, self.viewer.state.title)
else:
plotter.export_obj(filepath)


@viewer_tool
class ARLocalQRTool(Tool):
Expand Down
15 changes: 12 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,25 @@ def data_files(root_directory):
install_requires=[
"gltflib",
"glue-core",
"glue-qt",
"glue-vispy-viewers",
"ngrok",
"pillow",
"pyvista",
"segno"
],
extras_require={
"test": [
"flake8"
],
"qt": [
"glue-qt"
],
"jupyter": [
"glue-jupyter",
"ipyfilechooser",
"ipyvuetify"
],
"qr": [
"ngrok",
"segno"
]
},
entry_points={
Expand Down

0 comments on commit 8bc6b9c

Please sign in to comment.