Skip to content

Commit

Permalink
Merge pull request #38 from Carifio24/update-mv-qr
Browse files Browse the repository at this point in the history
Fix QR tool and update model-viewer HTML
  • Loading branch information
Carifio24 authored Aug 12, 2024
2 parents 4dfa3d6 + 9fac441 commit 7fa5191
Show file tree
Hide file tree
Showing 8 changed files with 155 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 *
149 changes: 36 additions & 113 deletions glue_ar/common/export.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
from collections import defaultdict
from os.path import abspath, dirname, join, splitext
from os.path import join, extsep, split, 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")


_BUILDERS = {
"gltf": GLTFBuilder,
"glb": GLTFBuilder,
Expand All @@ -36,7 +37,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 = _BUILDERS[ext]()
layer_groups = defaultdict(list)
export_groups = defaultdict(list)
Expand All @@ -58,12 +60,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 @@ -73,115 +79,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 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 7fa5191

Please sign in to comment.