Hi All,
This is my second post about this but I’m trying to embed a 3js viewer in my streamlit app and am having trouble. The viewer just doesn’t render. What is the solution to this?
Thanks
import streamlit as st
import streamlit.components.v1 as components
html_string = '''
<h1>HTML</h1>
<script language="javascript">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { Rhino3dmLoader } from 'three/addons/loaders/3DMLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
let camera, scene, renderer;
let controls, gui;
init();
animate();
function init() {
THREE.Object3D.DEFAULT_UP.set( 0, 0, 1 );
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( 2 );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.outputEncoding = THREE.sRGBEncoding;
document.body.appendChild( renderer.domElement );
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 10000 );
camera.position.set( 40, -40, 50 );
scene = new THREE.Scene();
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight( 0xffffff, 2 );
directionalLight.position.set( 20, 40, 100);
scene.add( directionalLight );
const loader = new Rhino3dmLoader();
loader.setLibraryPath( 'https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/' );
loader.load( '/upload/output.3dm', function ( object ) {
scene.add( object );
initGUI( object.userData.layers );
// hide spinner
document.getElementById( 'loader' ).style.display = 'none';
} );
controls = new OrbitControls( camera, renderer.domElement );
controls.enableZoom = true;
controls.enableDamping = true;
controls.dampingDactor = 0.05;
controls.minDistance = 100;
controls.maxDistance = 500;
controls.maxPolarAngle = Math.PI / 2;
window.addEventListener( 'resize', resize );
function resize() {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize( width, height );
}
function animate() {
controls.update();
renderer.render( scene, camera );
renderer.setPixelRatio(2);
requestAnimationFrame( animate );
}
function initGUI( layers ) {
gui = new GUI({
title: 'test',
});
for ( let i = 0; i < layers.length; i ++ ) {
const layer = layers[ i ];
gui.add( layer, 'visible' ).name( layer.name ).onChange( function ( val ) {
const name = this.object.name;
scene.traverse( function ( child ) {
if ( child.userData.hasOwnProperty( 'attributes' ) ) {
if ( 'layerIndex' in child.userData.attributes ) {
const layerName = layers[ child.userData.attributes.layerIndex ].name;
if ( layerName === name ) {
child.visible = val;
layer.visible = val;
}
}
}
} );
} );
}
}
</script> '''
components.html(html_string)
st.markdown(html_string, unsafe_allow_html=True)
1 Like
I dont know if this has been answered, but st.markdown
doesnt execute javascript code. I believe there is a custom component that does (I will try to find it and edit this comment).
Edit: So I was thinking of another component, but the component.html
should execute javascript fine.
I think you forgot to add the threejs script elements. An old project of mine had the following:
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.js"></script>
<script type="module">
import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js';
import { OrbitControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js';
// rest of code here
</script>
I think this shows two ways to import. You can use cdnjs links in import statement or separate script elements. Basically, there has to be a way to find/get the javascript modules you are referencing
One of the custom components I built has the ability to execute javascript, but its for html presentation slides.
If you didnt already find a solution and still need a component that allows you to execute threeJS code and display result, I could build one pretty easily using code from the aforementioned component.
I dont know how much demand there is for something like this. So let me know.
Hi @bouzidanas,
Thanks for the reply, I tried components, but it didn’t work. I don’t think I am missing any imports because I used this same code in my flask application and it worked fine.
This is what I tried.
import streamlit as st
import streamlit.components.v1 as components
components.html('''
<h1>HTML</h1>
<div id="viewer"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.js"></script>
<script type="module">
import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js';
import { OrbitControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js';
import { Rhino3dmLoader } from 'https://cdn.skypack.dev/three/examples/jsm/loaders/3DMLoader.js';
import { GUI } from 'https://cdn.skypack.dev/three/examples/jsm/libs/dat.gui.module.js';
let camera, scene, renderer;
let controls, gui;
init();
animate();
function init() {
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('viewer').appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.set(40, -40, 50);
scene = new THREE.Scene();
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 2);
directionalLight.position.set(20, 40, 100);
scene.add(directionalLight);
const loader = new Rhino3dmLoader();
loader.setLibraryPath('https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/');
loader.load('./output.3dm', function (object) {
scene.add(object);
initGUI(object.userData.layers);
});
controls = new OrbitControls(camera, renderer.domElement);
controls.enableZoom = true;
controls.enableDamping = true;
controls.dampingDactor = 0.05;
controls.minDistance = 100;
controls.maxDistance = 500;
controls.maxPolarAngle = Math.PI / 2;
window.addEventListener('resize', resize);
}
function resize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
function initGUI(layers) {
gui = new GUI({ title: 'test' });
for (let i = 0; i < layers.length; i++) {
const layer = layers[i];
gui.add(layer, 'visible').name(layer.name).onChange(function (val) {
const name = this.object.name;
scene.traverse(function (child) {
if (child.userData.hasOwnProperty('attributes')) {
if ('layerIndex' in child.userData.attributes) {
const layerName = layers[child.userData.attributes.layerIndex].name;
if (layerName === name) {
child.visible = val;
layer.visible = val;
}
}
}
});
});
}
}
</script>
''',
height=600)```
According to the console, something is wrong with the imports you added (response code 500). I just tested an example I have to see if I can get ThreeJS working and it works.
The example:
import streamlit as st
import streamlit.components.v1 as components
components.html('''
<style>
*
{
margin: 0;
padding: 0;
}
html,
body
{
overflow: hidden;
min-height: 700px;
}
.webgl
{
position: fixed;
top: 0;
left: 0;
outline: none;
}
</style>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<!--<script src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.js"></script> -->
<!-- <script src="http://threejs.org/examples/js/controls/TrackballControls.js"></script> -->
<script type="module">
import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js';
import { OrbitControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js';
//import { TrackballControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/TrackballControls.js';
// Base
// ----------
// Initialize scene
const scene = new THREE.Scene()
// Initialize camera
const camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.1, 60)
// Reposition camera
camera.position.set(6, 0, 0)
// Initialize renderer
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true
})
// Set renderer size
renderer.setSize(window.innerWidth, window.innerHeight)
// Append renderer to body
document.body.appendChild(renderer.domElement)
// Initialize controls
const controls = new OrbitControls(camera, renderer.domElement)
// World
// ----------
// Load world texture
const worldTexture = new THREE.TextureLoader().load("https://assets.codepen.io/141041/small-world.jpg")
// Initialize world geometry
const worldGeometry = new THREE.SphereGeometry(1, 40, 40)
// Initialize world material
const worldMaterial = new THREE.MeshLambertMaterial({
map: worldTexture
})
// Initialize world
const world = new THREE.Mesh(worldGeometry, worldMaterial)
// Add earth to scene
scene.add(world)
// Clouds
// ----------
// Load clouds texture
const cloudTexture = new THREE.TextureLoader().load("https://assets.codepen.io/141041/small-world-clouds.png")
// Initialize clouds geometry
const cloudGeometry = new THREE.SphereGeometry(1.01, 40, 40)
// Initialize clouds material
const cloudMaterial = new THREE.MeshBasicMaterial({
map: cloudTexture,
transparent: true
})
// Initialize clouds
const clouds = new THREE.Mesh(cloudGeometry, cloudMaterial)
// Add clouds to scene
scene.add(clouds)
// add subtle ambient lighting
const ambientLight = new THREE.AmbientLight(0xbbbbbb);
scene.add(ambientLight);
// directional lighting
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(1, 1, 1).normalize();
scene.add(directionalLight);
// Animation
// ----------
// Prepare animation loop
function animate() {
// Request animation frame
requestAnimationFrame(animate)
// Rotate world
world.rotation.y += 0.0005
// Rotate clouds
clouds.rotation.y -= 0.001
// Render scene
renderer.render(scene, camera)
}
// Animate
animate()
// Resize
// ----------
// Listen for window resizing
window.addEventListener('resize', () => {
// Update camera aspect
camera.aspect = window.innerWidth / window.innerHeight
// Update camera projection matrix
camera.updateProjectionMatrix()
// Resize renderer
renderer.setSize(window.innerWidth, window.innerHeight)
});
</script>
<style>
body{
background: radial-gradient(circle at center, white, rgba(113,129,191,0.5) 50%);
}
</style>
''',
height=600)
see if this works for you so that you can identify if there is something wrong with your setup.
Hi @bouzidanas,
Looks like theres some progress but still not showing up anything.
So I imported the libraries properly. Theres no visible errors so I’m not sure. The 3dm is in the same folder as my .py
import streamlit.components.v1 as components
components.html('''
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script>
<script type="module">
import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js';
import { OrbitControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js';
import { Rhino3dmLoader } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/loaders/3DMLoader.js';
let camera, scene, renderer;
let controls, gui;
init();
animate();
function init() {
THREE.Object3D.DEFAULT_UP.set( 0, 0, 1 );
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( 2 );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.outputEncoding = THREE.sRGBEncoding;
document.body.appendChild( renderer.domElement );
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 10000 );
camera.position.set( 40, -40, 50 );
scene = new THREE.Scene();
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight( 0xffffff, 2 );
directionalLight.position.set( 20, 40, 100);
scene.add( directionalLight );
const loader = new Rhino3dmLoader();
loader.setLibraryPath( 'https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/' );
loader.load( './output.3dm', function ( object ) {
scene.add( object );
initGUI( object.userData.layers );
// hide spinner
document.getElementById( 'loader' ).style.display = 'none';
} );
controls = new OrbitControls( camera, renderer.domElement );
controls.enableZoom = true;
controls.enableDamping = true;
controls.dampingDactor = 0.05;
controls.minDistance = 100;
controls.maxDistance = 500;
controls.maxPolarAngle = Math.PI / 2;
window.addEventListener( 'resize', resize );
function resize() {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize( width, height );
}
function animate() {
controls.update();
renderer.render( scene, camera );
renderer.setPixelRatio(2)
requestAnimationFrame( animate );
}
}
</script>
''',
height=600)
Thanks
I think there are multiple issues now. Change,
THREE.Object3D.DEFAULT_UP.set( 0, 0, 1 );
to
THREE.Object3D.DefaultUp.set( 0, 0, 1 );
and then the next issue is that output.3dm
is not found
you might have to straight up include the contents or figure out where to put it so that the component.html can find it
Yea I’m not sure how to include it straight up because I don’t think it’s possible, is there another way to reference the file?
I think this comment might provide a solution. Alternatively, you can put the file somewhere in the cloud where you can access it by url and that might be a better solution.
Thanks for the help @bouzidanas
I might just skip this and not pursue this issue further as streamlit currently just lacks the capabilities, but hopefully JS becomes more easily usable within streamlit
Thanks for all your help.
Hi fren,
I believe the issue you’re facing is that the JavaScript code within the HTML string is not executed properly within the Streamlit app. Streamlit’s components.html
function does not automatically execute JavaScript code. To embed a 3D viewer built with Three.js in a Streamlit app, you can follow these steps:
- Move the JavaScript code to a separate JavaScript file (e.g.,
viewer.js
). Remove the <script>
tags from the HTML string and leave only the contents of the <script>
tag.
// viewer.js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { Rhino3dmLoader } from 'three/addons/loaders/3DMLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
let camera, scene, renderer;
let controls, gui;
function init() {
// Remaining code...
}
function animate() {
// Remaining code...
}
function initGUI(layers) {
// Remaining code...
}
init();
animate();
- Place the JavaScript code in a separate file (
viewer.js
) in the same directory as your Streamlit app script.
- Modify your Streamlit app script to include the
viewer.js
file using the components.html
function.
import streamlit as st
import streamlit.components.v1 as components
html_string = '''
<h1>HTML</h1>
'''
components.html(html_string)
# Load and embed the JavaScript file
with open("viewer.js", "r") as js_file:
js_code = js_file.read()
components.html(js_code)
Ensure the viewer.js
file is in the same directory as your Streamlit app script. When the Streamlit app runs, it will embed the viewer.js
file using the components.html
function, and the JavaScript code will be executed correctly, rendering the 3D viewer.
I hope it helps you to find out the solution, if there is anything in which I can help you can reach out to me.
Hi @ShivamAgarwal-code,
I tried doing this however it seems like the output is just text of the js code on the streamlit website.
Thanks