Spaces:
Running
Running
import streamlit as st | |
import os, base64, shutil, random | |
from pathlib import Path | |
def load_aframe_and_extras(): | |
return """ | |
<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script> | |
<script src="https://unpkg.com/[email protected]/dist/aframe-event-set-component.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/aframe-volumetric-fog@latest/dist/aframe-volumetric-fog.min.js"></script> | |
<script> | |
let score = 0; | |
AFRAME.registerComponent('draggable', { | |
init: function () { | |
this.el.setAttribute('class', 'raycastable'); | |
this.el.setAttribute('cursor-listener', ''); | |
this.dragHandler = this.dragMove.bind(this); | |
this.el.sceneEl.addEventListener('mousemove', this.dragHandler); | |
this.el.addEventListener('mousedown', this.onDragStart.bind(this)); | |
this.el.addEventListener('mouseup', this.onDragEnd.bind(this)); | |
this.camera = document.querySelector('[camera]'); | |
}, | |
remove: function () { | |
this.el.removeAttribute('cursor-listener'); | |
this.el.sceneEl.removeEventListener('mousemove', this.dragHandler); | |
}, | |
onDragStart: function (evt) { | |
this.isDragging = true; | |
this.el.emit('dragstart'); | |
}, | |
onDragEnd: function (evt) { | |
this.isDragging = false; | |
this.el.emit('dragend'); | |
}, | |
dragMove: function (evt) { | |
if (!this.isDragging) return; | |
var camera = this.camera.getObject3D('camera'); | |
var worldPos = new THREE.Vector3(); | |
camera.getWorldPosition(worldPos); | |
var worldDir = new THREE.Vector3(); | |
camera.getWorldDirection(worldDir); | |
var camX = worldPos.x; | |
var camY = worldPos.y; | |
var camZ = worldPos.z; | |
var dirX = worldDir.x; | |
var dirY = worldDir.y; | |
var dirZ = worldDir.z; | |
var t = -camY / dirY; | |
var moveX = camX + t * dirX; | |
var moveZ = camZ + t * dirZ; | |
this.el.setAttribute('position', moveX + " " + 0.0 + " " + moveZ); | |
} | |
}); | |
AFRAME.registerComponent('undulating-rotation', { | |
schema: { | |
speed: { type: 'number', default: 1 }, // Base rotation speed | |
amplitude: { type: 'number', default: 20 }, // Maximum rotation in degrees | |
frequency: { type: 'number', default: 0.5 } // Speed of undulation | |
}, | |
init: function () { | |
this.initialRotation = Math.random() * 360; // Random initial rotation | |
this.time = 0; | |
}, | |
tick: function (time, deltaTime) { | |
this.time += deltaTime / 1000; | |
const rotationAmount = Math.sin(this.time * this.data.frequency) * this.data.amplitude; | |
this.el.setAttribute('rotation', { x: 0, y: this.initialRotation + rotationAmount, z: 0 }); | |
} | |
}); | |
</script> | |
""" | |
def encode_file(file_path): | |
with open(file_path, "rb") as file: | |
return base64.b64encode(file.read()).decode() | |
def generate_tilemap(files, directory, grid_width, grid_height, max_unique_models=5): | |
assets = "<a-assets>" | |
entities = "" | |
tile_size = 1 | |
start_x = -(grid_width * tile_size) / 2 | |
start_z = -(grid_height * tile_size) / 2 | |
available_models = [f for f in files if f.endswith(('.obj', '.glb'))] | |
available_images = [f for f in files if f.endswith(('.webp', '.png'))] | |
encoded_files = {} | |
unique_files = random.sample(files, min(len(files), max_unique_models)) | |
for file in unique_files: | |
file_path = os.path.join(directory, file) | |
file_type = file.split('.')[-1] | |
encoded_file = encode_file(file_path) | |
encoded_files[file] = encoded_file | |
if file_type in ['obj', 'glb']: | |
assets += f'<a-asset-item id="{Path(file).stem}" src="data:application/octet-stream;base64,{encoded_file}"></a-asset-item>' | |
elif file_type in ['webp', 'png']: | |
mime_type = f"image/{file_type}" | |
assets += f'<img id="{Path(file).stem}" src="data:{mime_type};base64,{encoded_file}" />' | |
assets += "</a-assets>" | |
for i in range(grid_width): | |
for j in range(grid_height): | |
x = start_x + (i * tile_size) | |
z = start_z + (j * tile_size) | |
position = f"{x} 0 {z}" | |
chosen_file = random.choice(files) | |
file_type = chosen_file.split('.')[-1] | |
file_stem = Path(chosen_file).stem | |
rotation = f"0 {random.uniform(0, 360)} 0" | |
bounce_speed = f"{random.uniform(0.01, 0.03)} {random.uniform(0.01, 0.03)} {random.uniform(0.01, 0.03)}" | |
amplitude = random.uniform(5, 15) | |
frequency = random.uniform(0.1, 0.5) | |
if file_type == 'obj': | |
entities += f'<a-entity position="{position}" rotation="{rotation}" scale="0.5 0.5 0.5" obj-model="obj: #{file_stem}" class="raycastable" draggable undulating-rotation="amplitude: {amplitude}; frequency: {frequency}"></a-entity>' | |
elif file_type == 'glb': | |
entities += f'<a-entity position="{position}" rotation="{rotation}" scale="0.5 0.5 0.5" gltf-model="#{file_stem}" class="raycastable" draggable undulating-rotation="amplitude: {amplitude}; frequency: {frequency}"></a-entity>' | |
elif file_type in ['webp', 'png']: | |
if available_images: | |
random_image = random.choice(available_images) | |
image_stem = Path(random_image).stem | |
entities += f'<a-plane position="{position}" rotation="-90 0 0" width="{tile_size}" height="{tile_size}" material="src: #{image_stem}" class="raycastable" draggable></a-plane>' | |
return assets, entities | |
def main(): | |
st.set_page_config(layout="wide") | |
with st.sidebar: | |
st.markdown("### 🤖 3D AI Using Claude 3.5 Sonnet for AI Pair Programming") | |
st.markdown("[Open 3D Animation Toolkit](https://huggingface.co/spaces/awacke1/3d_animation_toolkit)", unsafe_allow_html=True) | |
st.markdown("### ⬆️ Upload") | |
uploaded_files = st.file_uploader("Add files:", accept_multiple_files=True, key="file_uploader") | |
st.markdown("### 🗺️ Grid Size") | |
grid_width = st.slider("Grid Width", 1, 10, 8) | |
grid_height = st.slider("Grid Height", 1, 10, 5) | |
st.markdown("### ℹ️ Instructions") | |
st.write("- Click and drag to move objects") | |
st.write("- Mouse wheel to zoom") | |
st.write("- Right-click and drag to rotate view") | |
st.markdown("### 📁 Directory") | |
directory = st.text_input("Enter path:", ".", key="directory_input") | |
if not os.path.isdir(directory): | |
st.sidebar.error("Invalid directory path") | |
return | |
file_types = ['obj', 'glb', 'webp', 'png'] | |
if uploaded_files: | |
for uploaded_file in uploaded_files: | |
file_extension = Path(uploaded_file.name).suffix.lower()[1:] | |
if file_extension in file_types: | |
with open(os.path.join(directory, uploaded_file.name), "wb") as f: | |
shutil.copyfileobj(uploaded_file, f) | |
st.sidebar.success(f"Uploaded: {uploaded_file.name}") | |
else: | |
st.sidebar.warning(f"Skipped unsupported file: {uploaded_file.name}") | |
files = [f for f in os.listdir(directory) if f.split('.')[-1] in file_types] | |
aframe_scene = f""" | |
<a-scene embedded style="height: 600px; width: 100%;"> | |
{load_aframe_and_extras()} | |
<a-entity id="rig" movement-controls="fly: true; speed: 0.3" position="0 1.6 0"> | |
<a-camera id="camera" position="0 1.6 10"></a-camera> | |
</a-entity> | |
<a-entity light="type: ambient; color: #EEE"></a-entity> | |
<a-entity light="type: directional; color: #FFF; intensity: 0.6" position="-0.5 1 1"></a-entity> | |
<a-sky color="#ECF0F1"></a-sky> | |
<a-entity id="tilemap"> | |
{''.join(generate_tilemap(files, directory, grid_width, grid_height))} | |
</a-entity> | |
</a-scene> | |
""" | |
st.components.v1.html(aframe_scene, height=600) | |
if __name__ == "__main__": | |
main() |