Skip to content

Commit

Permalink
Merge branch 'master' into add-scatter-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Carifio24 authored Aug 13, 2024
2 parents 8b8b526 + 5b9f8ae commit fa7ce48
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 121 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
recursive-include glue_ar/js *
recursive-include glue_ar/resources *
Binary file modified examples/edenhofer-isosurface-256-nocomp.usdz
Binary file not shown.
148 changes: 35 additions & 113 deletions glue_ar/common/export.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
from collections import defaultdict
from inspect import getfullargspec
from os.path import abspath, dirname, join, splitext
from os.path import abspath, dirname, extsep, join, splitext
from string import Template
from subprocess import run
from typing import Dict
from glue.core.state_objects import State
from glue.config import settings
from glue_vispy_viewers.scatter.viewer_state import Vispy3DViewerState
from glue_vispy_viewers.volume.layer_state import VolumeLayerState


from glue_ar.common.export_options import ar_layer_export
from glue_ar.common.gltf_builder import GLTFBuilder
from glue_ar.common.usd_builder import USDBuilder
from glue_ar.utils import Bounds, BoundsWithResolution, export_label_for_layer
from glue_ar.utils import PACKAGE_DIR, RESOURCES_DIR, Bounds, BoundsWithResolution, export_label_for_layer

from typing import List, Tuple, Union


NODE_MODULES_DIR = join(abspath(join(dirname(abspath(__file__)), "..")),
"js", "node_modules")

NODE_MODULES_DIR = join(PACKAGE_DIR, "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")
Expand All @@ -38,7 +38,8 @@ def export_viewer(viewer_state: Vispy3DViewerState,
state_dictionary: Dict[str, Tuple[str, State]],
filepath: str):

ext = splitext(filepath)[1][1:]
base, ext = splitext(filepath)
ext = ext[1:]
builder_cls = _BUILDERS[ext]
count = len(getfullargspec(builder_cls.__init__)[0])
builder_args = [filepath] if count > 1 else []
Expand All @@ -63,12 +64,16 @@ def export_viewer(viewer_state: Vispy3DViewerState,

builder.build_and_export(filepath)

if ext in ["gltf", "glb"]:
mv_path = f"{base}{extsep}html"
export_modelviewer(mv_path, filepath, viewer_state.title)


def compress_gltf_pipeline(filepath):
def compress_gltf_pipeline(filepath: str):
run(["node", GLTF_PIPELINE_FILEPATH, "-i", filepath, "-o", filepath, "-d"], capture_output=True)


def compress_gltfpack(filepath):
def compress_gltfpack(filepath: str):
run(["node", GLTFPACK_FILEPATH, "-i", filepath, "-o", filepath], capture_output=True)


Expand All @@ -78,115 +83,32 @@ def compress_gltfpack(filepath):
}


def compress_gl(filepath, method="draco"):
def compress_gl(filepath: str, method: str = "draco"):
compressor = COMPRESSORS.get(method, None)
if compressor is None:
raise ValueError("Invalid compression method specified")
compressor(filepath)


def export_modelviewer(output_path, gltf_path, alt_text):
def export_modelviewer(output_path: str, gltf_path: str, alt_text: str):
mv_url = "https://ajax.googleapis.com/ajax/libs/model-viewer/3.3.0/model-viewer.min.js"
html = f"""
<html>
<body>
<script type="module" src="{mv_url}"></script>
<style>
model-viewer {{
width: 100%;
height: 100%;
}}
/* This keeps child nodes hidden while the element loads */
:not(:defined) > * {{
display: none;
}}
.ar-button {{
background-repeat: no-repeat;
background-size: 20px 20px;
background-position: 12px 50%;
background-color: #fff;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 16px;
padding: 0px 16px 0px 40px;
font-family: Roboto Regular, Helvetica Neue, sans-serif;
font-size: 14px;
color:#4285f4;
height: 36px;
line-height: 36px;
border-radius: 18px;
border: 1px solid #DADCE0;
}}
.ar-button:active {{
background-color: #E8EAED;
}}
.ar-button:focus {{
outline: none;
}}
.ar-button:focus-visible {{
outline: 1px solid #4285f4;
}}
.hotspot {{
position: relative;
background: #ddd;
border-radius: 32px;
box-sizing: border-box;
border: 0;
--min-hotspot-opacity: 0.5;
width: 24px;
height: 24px;
padding: 8px;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
}}
.hotspot:focus {{
border: 4px solid rgb(0, 128, 200);
width: 32px;
height: 32px;
outline: none;
}}
.hotspot > * {{
transform: translateY(-50%);
opacity: 1;
}}
.hotspot:not([data-visible]) > * {{
pointer-events: none;
opacity: 0;
transform: translateY(calc(-50% + 4px));
transition: transform 0.3s, opacity 0.3s;
}}
.info {{
display: block;
position: absolute;
font-family: Futura, Helvetica Neue, sans-serif;
color: rgba(0, 0, 0, 0.8);
font-weight: 700;
font-size: 18px;
max-width: 128px;
padding: 0.5em 1em;
background: #ddd;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
left: calc(100% + 1em);
top: 50%;
}}
</style>
<model-viewer
src="{gltf_path}"
camera-orbit="0.9677rad 1.2427rad auto"
shadow-intensity="1"
ar
ar-modes="webxr quick-look"
camera-controls
alt="{alt_text}"
>
<button slot="ar-button" class="ar-button">View in your space</button>
</model-viewer>
</body>
</html>
"""

with open(output_path, 'w') as f:
f.write(html)
with open(join(RESOURCES_DIR, "model-viewer.html")) as f:
html_template = f.read()
with open(join(RESOURCES_DIR, "model-viewer.css")) as g:
css_template = g.read()
css = Template(css_template).substitute({"bg_color": settings.BACKGROUND_COLOR})
style = f"<style>{css}</style>"

_, gltf_name = split(gltf_path)

substitutions = {
"url": mv_url,
"gltf_path": gltf_name,
"alt_text": alt_text,
"style": style,
"button_text": "View in AR",
}
html = Template(html_template).substitute(substitutions)

with open(output_path, 'w') as of:
of.write(html)
2 changes: 1 addition & 1 deletion glue_ar/qt/qr_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ def __init__(self, parent, url=None, img=None):
self.ui.label_url.setText(f"<a href=\"{url}\">Open 3D view</a>")
self.ui.label_image.setPixmap(self.pix)
width, height = img.size
self.setFixedSize(width, height + 30)
self.setFixedSize(width, height + 60)
27 changes: 23 additions & 4 deletions glue_ar/qt/qr_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@
from os.path import split
from tempfile import NamedTemporaryFile
from threading import Thread
from typing import Type

from glue.config import viewer_tool
from glue.core.state_objects import State
from glue.viewers.common.state import LayerState
from glue.viewers.common.tool import Tool
from glue_vispy_viewers.scatter.layer_artist import ScatterLayerState
from glue_vispy_viewers.volume.volume_viewer import VispyVolumeViewerMixin

from glue_ar.utils import AR_ICON
from glue_ar.common.export import create_plotter, export_gl, export_modelviewer
from glue_ar.utils import AR_ICON, export_label_for_layer, xyz_bounds
from glue_ar.common.export import export_modelviewer, export_viewer
from glue_ar.common.scatter_export_options import ARScatterExportOptions
from glue_ar.common.volume_export_options import ARIsosurfaceExportOptions
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
Expand All @@ -24,13 +31,25 @@ class ARLocalQRTool(Tool):
action_text = "3D view via QR"
tool_tip = "Get a QR code for the current view in 3D"

def _export_items_for_layer(self, layer: LayerState) -> Type[State]:
if isinstance(layer, ScatterLayerState):
return ("Scatter", ARScatterExportOptions())
else:
return ("Isosurface", ARIsosurfaceExportOptions(isosurface_count=8))

def activate(self):
layer_states = [layer.state for layer in self.viewer.layers
if layer.enabled and layer.state.visible]
bounds = xyz_bounds(self.viewer.state, with_resolution=isinstance(self.viewer, VispyVolumeViewerMixin))
state_dictionary = {
export_label_for_layer(state): self._export_items_for_layer(state)
for state in layer_states
}
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_viewer(self.viewer.state, layer_states, bounds, state_dictionary, gltf_tmp.name)
export_modelviewer(html_tmp.name, gltf_base, self.viewer.state.title)

port = 4000
Expand Down
67 changes: 67 additions & 0 deletions glue_ar/resources/model-viewer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
:root {
--glue-red: #eb1c24;
}

body {
margin: 0;
background-color: $bg_color;
}

model-viewer {
width: 100%;
height: 100%;
}

/* This keeps child nodes hidden while the element loads */
:not(:defined) > * {
display: none;
}
.ar-button {
background-repeat: no-repeat;
background-size: 20px 20px;
background-position: 12px 50%;
background-color: #f5c6c888;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 16px;
padding: 0px 16px 0px 40px;
font-family: Roboto Regular, Helvetica Neue, sans-serif;
font-size: 60pt;
font-weight: bold;
color: var(--glue-red);
height: 200px;
width: max(300px, 80%);
border-radius: 18px;
border: 5px solid var(--glue-red);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 10px;
}
.ar-button:active {
background-color: #E8EAED;
}
.ar-button:focus {
outline: none;
}
.ar-button:focus-visible {
outline: 1px solid #1f5ef1;
}
.info {
display: block;
position: absolute;
font-family: Futura, Helvetica Neue, sans-serif;
color: rgba(0, 0, 0, 0.8);
font-weight: 700;
font-size: 18px;
max-width: 128px;
padding: 0.5em 1em;
background: #ddd;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
left: calc(100% + 1em);
top: 50%;
}

22 changes: 22 additions & 0 deletions glue_ar/resources/model-viewer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<html>
<head>
<script type="module" src="$url"></script>
$style
</head>
<body>
<model-viewer
src="$gltf_path"
camera-orbit="0.9677rad 1.2427rad auto"
shadow-intensity="1"
ar
ar-modes="webxr quick-look"
camera-controls
alt="$alt_text"
>
<button slot="ar-button" class="ar-button">
$button_text
</button>
</model-viewer>
</body>
</html>

6 changes: 4 additions & 2 deletions glue_ar/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import os
from os.path import abspath, dirname, join
from uuid import uuid4
from glue.core import BaseData
from glue.core.subset_group import GroupedSubset
Expand All @@ -12,7 +12,9 @@
from typing import Iterator, Literal, overload, Iterable, List, Optional, Tuple, Union


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

Bounds = List[Tuple[float, float]]
BoundsWithResolution = List[Tuple[float, float, int]]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ def data_files(root_directory):
zip_safe=False,
packages=find_packages(name, exclude=["js"]),
package_data={
"glue_ar": ["py.typed"],
"glue_ar": ["py.typed", "resources/**"],
},
include_package_data=True,
install_requires=[
Expand Down

0 comments on commit fa7ce48

Please sign in to comment.