Skip to content

Commit

Permalink
Merge pull request #4 from edsaac/tjs_minhtml
Browse files Browse the repository at this point in the history
v0.0.5 - Transparency and rotation
  • Loading branch information
edsaac authored Nov 9, 2022
2 parents 0102a60 + 5cc4035 commit 967ed05
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 62 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,5 @@ dmypy.json
# Pyre type checker
.pyre/

gitignored/
gitignored/
.experimental/
10 changes: 9 additions & 1 deletion stpyvista/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,19 @@ stpyvista(plotter, key="pv_cube")

## Log changes

<details>
<summary>
v 0.0.5
</summary>
- Support transparent backgrounds to blend with streamlit's web app theme.
- Add a control to spin along a certain axis the first mesh passed to the plotter.
</details>

<details>
<summary>
v 0.0.4
</summary>
- Pass a key to the stpyvista component to avoid re-rendering at every streamlit interaction
- Using ipywidgets `embed_minimal_html` directly instead of pyvista `export_html`.
- Update examples as a multipage streamlit app
<details>
</details>
2 changes: 1 addition & 1 deletion stpyvista/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "stpyvista"
version = "0.0.4"
version = "0.0.5"
authors = [
{ name="Edwin S", email="[email protected]" },
]
Expand Down
2 changes: 1 addition & 1 deletion stpyvista/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

setuptools.setup(
name="stpyvista",
version="0.0.4",
version="0.0.5",
author="Edwin S",
author_email="[email protected]",
description="Streamlit component that allows you to visualize pyvista 3D visualizations",
Expand Down
142 changes: 88 additions & 54 deletions stpyvista/src/stpyvista/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# __init__.py

from pathlib import Path
from typing import Optional
from typing import Optional, Union

import streamlit as st
import streamlit.components.v1 as components
Expand All @@ -12,6 +12,8 @@
from io import StringIO
from ipywidgets.embed import embed_minimal_html

import pythreejs as tjs

# Tell streamlit that there is a component called stpyvista,
# and that the code to display that component is in the "frontend" folder
frontend_dir = (Path(__file__).parent / "frontend").absolute()
Expand All @@ -21,6 +23,26 @@
path=str(frontend_dir)
)

def get_Meshes(renderer: tjs.Renderer) -> list[tjs.Mesh]:
return [child for child in renderer._trait_values["scene"].children if isinstance(child, tjs.Mesh)]

def spin_element_on_axis(renderer: tjs.Renderer, axis:str = "z", revolution_time:float = 4.0):

## Makes a full spin in a second
spin_track = tjs.NumberKeyframeTrack(name=f'.rotation[{axis}]', times=[0, revolution_time], values=[0, 6.28])
spin_clip = tjs.AnimationClip(tracks=[spin_track])

## Animate all meshes in scene
## This adds a separate control to all the meshes
## Need to implement this as a tjs.AnimationObjectGroup in the AnimationMixer,
## but that is not implemented pythreejs: https://github.com/jupyter-widgets/pythreejs/issues/372
# spin_action = [tjs.AnimationAction(tjs.AnimationMixer(mesh), spin_clip, mesh) for mesh in get_Meshes(renderer)]

# This adds controls for only the firts mesh in the plotter
mesh = get_Meshes(renderer)[0]
spin_action = [tjs.AnimationAction(tjs.AnimationMixer(mesh), spin_clip, mesh)]
return spin_action

class stpyvistaTypeError(TypeError):
""" Unsupported format for input? """
pass
Expand All @@ -38,33 +60,79 @@ class HTML_stpyvista:
plotter: pv.Plotter
Plotter to export to html
rotation: dict{"axis":'z', "revolution_time":float}
transparent_background: bool
"""
def __init__(self, plotter:pv.Plotter) -> None:
def __init__(
self,
plotter:pv.Plotter,
rotation:dict = None,
opacity_background:float = 0.0) -> None:

model_html = StringIO()
pv_to_tjs = plotter.to_pythreejs()
embed_minimal_html(model_html, pv_to_tjs, title="🧊-stpyvista")

## Animation controls
animations = []
if rotation:
animations = spin_element_on_axis(pv_to_tjs, **rotation)
else:
pass

# Build transparent background
## Remove background
pv_to_tjs._trait_values["scene"].background = None

## Support transparency
pv_to_tjs._alpha = True

## Retrieve intended color from pv.Plotter
pv_to_tjs.clearColor = plotter.background_color.hex_rgb

## Assign alpha
pv_to_tjs.clearOpacity = opacity_background

# Create HTML file
embed_minimal_html(model_html, [pv_to_tjs, *animations], title="🧊-stpyvista")
threejs_html = model_html.getvalue()
model_html.close()
dimensions = plotter.window_size

dimensions = plotter.window_size
self.threejs_html = threejs_html
self.window_dimensions = dimensions

# Create the python function that will be called from the front end
def stpyvista(
input : HTML_stpyvista,
input : Union[pv.Plotter, HTML_stpyvista],
horizontal_align : str = "center",
rotation : Union[bool, dict] = None,
opacity_background : float = 0.0,
key: Optional[str] = None
) -> None:
) -> None:

"""
Renders a HTML_stpyvista as a threejs model.
Parameters
----------
processed: dict
input: Union[pv.Plotter, HTML_stpyvista]
Plotter to render
horizontal_align: str = "center"
Either "center", "left" or "right"
rotation: dict = None
[Experimental]. Add a play button to spin the mesh along an axis.
If not False, expects a dictionary with keys {"axis": "z", "revolution_time":5}.
>> It only works for a single mesh, as it rotates the mesh rather than moving the camera
>> Also, edges are left behind - a lot to fix
opacity_background: float = 0.0
[Experimental]. Ignore background color.
key: str|None
An optional key that uniquely identifies this component. If this is
None, and the component's arguments are changed, the component will
Expand All @@ -75,64 +143,30 @@ def stpyvista(
None
"""

if isinstance(input, pv.Plotter):
input = HTML_stpyvista(input)
elif isinstance(input, HTML_stpyvista):
pass
else:
raise(stpyvistaTypeError)
if isinstance(input, pv.Plotter):
input = HTML_stpyvista(
input,
rotation=rotation,
opacity_background=opacity_background)
elif isinstance(input, HTML_stpyvista): pass
else: raise(stpyvistaTypeError)

if rotation: has_controls = 1.0
else: has_controls = 0.0

component_value = _component_func(
threejs_html = input.threejs_html,
width = input.window_dimensions[0],
height = input.window_dimensions[1],
horizontal_align = horizontal_align,
has_controls = has_controls,
key = key,
default = 0
)
default = 0)

return component_value

def main():
pass

if "carburator" not in st.session_state:
pl = pv.Plotter(window_size=[400,300])
mesh = pv.examples.download_carburator()
pl.set_background('white')
mesh.decimate(0.5, inplace=True)
pl.add_mesh(mesh, color='lightgrey', pbr=True, metallic=0.5)
pl.camera.zoom(2.0)
st.session_state.carburator = HTML_stpyvista(pl)

if "sphere" not in st.session_state:
pl = pv.Plotter(window_size=[300,200])
pl.set_background('#D3EEFF')
pl.add_mesh(pv.Sphere(center=(1, 0, 1)))
st.session_state.sphere = HTML_stpyvista(pl)

sphere = st.session_state.sphere
carburator = st.session_state.carburator

cols = st.columns([1,1.5])

with cols[0]:
st.title("Component `stpyvista`")
st.write("Show [**pyvista**](https://www.pyvista.org/) 3D visualizations in streamlit")
with cols[1]: stpyvista(carburator)

st.header("Different horizontal alignment")

with st.echo():
# The default is centered
stpyvista(sphere)

with st.echo():
# It can also go to the left
stpyvista(sphere, horizontal_align="left")

with st.echo():
# Or to the right
stpyvista(sphere, horizontal_align="right")

if __name__ == "__main__":
main()
17 changes: 13 additions & 4 deletions stpyvista/src/stpyvista/frontend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function onRender(event) {
if (!window.rendered) {

// You most likely want to get the data passed in like this
const {threejs_html, width, height, horizontal_align, key} = event.detail.args;
const {threejs_html, width, height, horizontal_align, has_controls, key} = event.detail.args;
const stpyvistadiv = document.getElementById("stpyvistadiv");
const stpyvistaframe = document.getElementById("stpyvistaframe");

Expand All @@ -30,15 +30,24 @@ function onRender(event) {
// just CSS styling does not apply to the iframe
stpyvistaframe.srcdoc = threejs_html;
stpyvistaframe.width = width + 15;
stpyvistaframe.height = height + 15;
console.log("WIDTH", width)
console.log("CONTROLS", has_controls)

if (has_controls > 0) {
stpyvistaframe.height = height + 60;
Streamlit.setFrameHeight(height + 95)
} else {
stpyvistaframe.height = height + 15;
Streamlit.setFrameHeight(height + 40)
}

stpyvistaframe.scrolling = "no";
stpyvistaframe.style.border = "none";

// Send some value to python
// Not very useful at the moment but keep it for later
// stpyvistadiv.addEventListener('click', event => sendValue(50), false);

Streamlit.setFrameHeight(height+40)

window.rendered = true;
}
}
Expand Down

0 comments on commit 967ed05

Please sign in to comment.