awacke1 commited on
Commit
3554ac8
ยท
verified ยท
1 Parent(s): 390ce77

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +270 -183
app.py CHANGED
@@ -1,204 +1,291 @@
1
  import streamlit as st
2
- import os
3
- import random
4
 
5
  @st.cache_data
6
- def scan_assets():
7
- """Discover textures, bump maps, glTF models, and OBJ(+MTL) pairs."""
8
- files = [f for f in os.listdir() if os.path.isfile(f)]
9
- img_exts = (".jpg", ".jpeg", ".png", ".gif")
10
-
11
- # Textures (exclude bump/normal)
12
- textures = [
13
- f for f in files
14
- if f.lower().endswith(img_exts)
15
- and not any(tag in f.lower() for tag in ("bump", "normal"))
16
- ]
17
-
18
- # Bump/NORMAL map (take the first one, if any)
19
- bump_maps = [
20
- f for f in files
21
- if f.lower().endswith(img_exts)
22
- and any(tag in f.lower() for tag in ("bump", "normal"))
23
- ]
24
-
25
- # glTF models
26
- gltf_models = [f for f in files if f.lower().endswith((".glb", ".gltf"))]
27
-
28
- # OBJ models + their MTL partners
29
- obj_models = [f for f in files if f.lower().endswith(".obj")]
30
- mtl_files = {
31
- os.path.splitext(f)[0]: f
32
- for f in files
33
- if f.lower().endswith(".mtl")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- models = []
37
- idx = 0
38
-
39
- # Register glTF entries
40
- for gltf in gltf_models:
41
- models.append({
42
- "type": "gltf",
43
- "asset_id": f"model{idx}",
44
- "src": gltf
45
- })
46
- idx += 1
 
 
47
 
48
- # Register OBJ entries
49
- for obj in obj_models:
50
- base = os.path.splitext(obj)[0]
51
- mtl = mtl_files.get(base)
52
- entry = {
53
- "type": "obj",
54
- "obj_id": f"model{idx}-obj",
55
- "obj": obj,
56
- "mtl_id": f"model{idx}-mtl" if mtl else None,
57
- "mtl": mtl
58
- }
59
- models.append(entry)
60
- idx += 1
61
 
62
- return textures, bump_maps, models
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
  def main():
65
- st.title("๐Ÿ”ณ A-Frame Tilemap with Mixed 3D Models")
66
- grid_size = st.sidebar.slider("Grid Size", 1, 20, 10)
67
-
68
- textures, bump_maps, models = scan_assets()
69
- if not textures or not models:
70
- st.warning("โš ๏ธ Drop at least one .jpg/.png and one .glb/.obj (with optional .mtl) in this folder.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  return
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- # --- Build <a-assets> ---
74
- asset_tags = []
75
- for i, tex in enumerate(textures):
76
- asset_tags.append(f'<img id="tex{i}" src="{tex}">')
77
-
78
- if bump_maps:
79
- asset_tags.append(f'<img id="bump0" src="{bump_maps[0]}">')
80
-
81
- for m in models:
82
- if m["type"] == "gltf":
83
- asset_tags.append(
84
- f'<a-asset-item id="{m["asset_id"]}" src="{m["src"]}"></a-asset-item>'
85
- )
86
- else:
87
- asset_tags.append(
88
- f'<a-asset-item id="{m["obj_id"]}" src="{m["obj"]}"></a-asset-item>'
89
- )
90
- if m["mtl_id"]:
91
- asset_tags.append(
92
- f'<a-asset-item id="{m["mtl_id"]}" src="{m["mtl"]}"></a-asset-item>'
93
- )
94
 
95
- assets_html = "\n ".join(asset_tags)
 
96
 
97
- # JS arrays for textures & models
98
- tex_js = ", ".join(f'"#tex{i}"' for i in range(len(textures)))
99
- models_js_elems = []
100
- for m in models:
101
- if m["type"] == "gltf":
102
- models_js_elems.append(f'{{type:"gltf", id:"#{m["asset_id"]}"}}')
103
  else:
104
- if m["mtl_id"]:
105
- models_js_elems.append(
106
- f'{{type:"obj", obj:"#{m["obj_id"]}", mtl:"#{m["mtl_id"]}"}}'
107
- )
108
- else:
109
- models_js_elems.append(
110
- f'{{type:"obj", obj:"#{m["obj_id"]}"}}'
111
- )
112
- models_js = ", ".join(models_js_elems)
113
 
114
- # Ground material (with optional bump)
115
- if bump_maps:
116
- ground_mat = "ground.setAttribute('material','color:#228B22; bumpMap:#bump0; bumpScale:0.2');"
117
- else:
118
- ground_mat = "ground.setAttribute('material','color:#228B22');"
119
-
120
- # --- Final HTML ---
121
- html = f"""
122
- <!DOCTYPE html>
123
- <html>
124
- <head>
125
- <meta charset="utf-8">
126
- <title>Tilemap Scene</title>
127
- <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
128
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/aframe-extras.loaders.min.js"></script>
129
- </head>
130
- <body>
131
- <a-scene>
132
- <a-assets>
133
- {assets_html}
134
- </a-assets>
135
-
136
- <!-- Lights -->
137
- <a-entity light="type: ambient; color: #BBB"></a-entity>
138
- <a-entity light="type: directional; color: #FFF; intensity:0.6" position="1 1 0"></a-entity>
139
- <a-entity light="type: point; intensity:0.6" position="0 5 0"></a-entity>
140
-
141
- <!-- Camera -->
142
- <a-entity camera look-controls position="0 {grid_size} {grid_size}"></a-entity>
143
-
144
- <!-- Tiles & Models -->
145
- <a-entity id="tilemap"></a-entity>
146
- </a-scene>
147
-
148
- <script>
149
- document.addEventListener('DOMContentLoaded', function() {{
150
- var scene = document.querySelector('a-scene');
151
- var tilemap = document.querySelector('#tilemap');
152
- var textures = [{tex_js}];
153
- var models = [{models_js}];
154
- var grid = {grid_size};
155
-
156
- for (var i = 0; i < grid; i++) {{
157
- for (var j = 0; j < grid; j++) {{
158
- var x = i - grid/2;
159
- var z = j - grid/2;
160
-
161
- // Base tile
162
- var tile = document.createElement('a-box');
163
- tile.setAttribute('width', 1);
164
- tile.setAttribute('height', 0.1);
165
- tile.setAttribute('depth', 1);
166
- var tidx = Math.floor(Math.random() * textures.length);
167
- tile.setAttribute('material', 'src:' + textures[tidx] + '; repeat:1 1');
168
- tile.setAttribute('position', x + ' 0 ' + z);
169
- tilemap.appendChild(tile);
170
-
171
- // Random model
172
- var m = models[Math.floor(Math.random() * models.length)];
173
- var ent = document.createElement('a-entity');
174
- if (m.type === 'gltf') {{
175
- ent.setAttribute('gltf-model', m.id);
176
- }} else {{
177
- var cmd = 'obj: ' + m.obj;
178
- if (m.mtl) cmd += '; mtl: ' + m.mtl;
179
- ent.setAttribute('obj-model', cmd);
180
- }}
181
- ent.setAttribute('scale', '0.5 0.5 0.5');
182
- ent.setAttribute('position', x + ' 0.5 ' + z);
183
- tilemap.appendChild(ent);
184
- }}
185
- }}
186
-
187
- // Ground plane
188
- var ground = document.createElement('a-plane');
189
- ground.setAttribute('width', grid * 2);
190
- ground.setAttribute('height', grid * 2);
191
- ground.setAttribute('rotation', '-90 0 0');
192
- {ground_mat}
193
- ground.setAttribute('position', '0 -0.05 0');
194
- scene.insertBefore(ground, scene.firstChild);
195
- }});
196
- </script>
197
- </body>
198
- </html>
199
  """
 
200
 
201
- st.components.v1.html(html, height=600, scrolling=False)
 
 
 
 
 
202
 
203
  if __name__ == "__main__":
204
  main()
 
1
  import streamlit as st
2
+ import os, base64, shutil, random
3
+ from pathlib import Path
4
 
5
  @st.cache_data
6
+ def load_aframe_and_extras():
7
+ return """
8
+ <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
9
+ <script src="https://unpkg.com/aframe-event-set-component@5.0.0/dist/aframe-event-set-component.min.js"></script>
10
+ <script>
11
+ let score = 0;
12
+ AFRAME.registerComponent('draggable', {
13
+ init: function () {
14
+ this.el.setAttribute('class', 'raycastable');
15
+ this.el.setAttribute('cursor-listener', '');
16
+ this.dragHandler = this.dragMove.bind(this);
17
+ this.el.sceneEl.addEventListener('mousemove', this.dragHandler);
18
+ this.el.addEventListener('mousedown', this.onDragStart.bind(this));
19
+ this.el.addEventListener('mouseup', this.onDragEnd.bind(this));
20
+ this.camera = document.querySelector('[camera]');
21
+ },
22
+ remove: function () {
23
+ this.el.removeAttribute('cursor-listener');
24
+ this.el.sceneEl.removeEventListener('mousemove', this.dragHandler);
25
+ },
26
+ onDragStart: function (evt) {
27
+ this.isDragging = true;
28
+ this.el.emit('dragstart');
29
+ },
30
+ onDragEnd: function (evt) {
31
+ this.isDragging = false;
32
+ this.el.emit('dragend');
33
+ },
34
+ dragMove: function (evt) {
35
+ if (!this.isDragging) return;
36
+ var camera = this.camera;
37
+ var vector = new THREE.Vector3(evt.clientX / window.innerWidth * 2 - 1, -(evt.clientY / window.innerHeight) * 2 + 1, 0.5);
38
+ vector.unproject(camera);
39
+ var dir = vector.sub(camera.position).normalize();
40
+ var distance = -camera.position.y / dir.y;
41
+ var pos = camera.position.clone().add(dir.multiplyScalar(distance));
42
+ this.el.setAttribute('position', pos);
43
+ }
44
+ });
45
+ AFRAME.registerComponent('bouncing', {
46
+ schema: {
47
+ speed: {type: 'vec3', default: {x: 0.1, y: 0.1, z: 0.1}},
48
+ dist: {type: 'vec3', default: {x: 0.5, y: 0.5, z: 0.5}}
49
+ },
50
+ init: function () {
51
+ this.originalPos = this.el.getAttribute('position');
52
+ this.dir = {x: 1, y: 1, z: 1};
53
+ },
54
+ tick: function (time, timeDelta) {
55
+ var currentPos = this.el.getAttribute('position');
56
+ var speed = this.data.speed;
57
+ var dist = this.data.dist;
58
+ ['x', 'y', 'z'].forEach(axis => {
59
+ currentPos[axis] += speed[axis] * this.dir[axis] * (timeDelta / 1000);
60
+ if (Math.abs(currentPos[axis] - this.originalPos[axis]) > dist[axis]) {
61
+ this.dir[axis] *= -1;
62
+ }
63
+ });
64
+ this.el.setAttribute('position', currentPos);
65
+ },
66
+ boost: function() {
67
+ var speed = this.data.speed;
68
+ ['x', 'y', 'z'].forEach(axis => {
69
+ speed[axis] *= 1.5;
70
+ });
71
+ this.data.speed = speed;
72
+ this.dir = {
73
+ x: Math.random() > 0.5 ? 1 : -1,
74
+ y: Math.random() > 0.5 ? 1 : -1,
75
+ z: Math.random() > 0.5 ? 1 : -1
76
+ };
77
+ }
78
+ });
79
+ AFRAME.registerComponent('moving-light', {
80
+ schema: {
81
+ color: {type: 'color', default: '#FFF'},
82
+ speed: {type: 'vec3', default: {x: 0.1, y: 0.1, z: 0.1}},
83
+ bounds: {type: 'vec3', default: {x: 5, y: 5, z: 5}}
84
+ },
85
+ init: function () {
86
+ this.dir = {x: 1, y: 1, z: 1};
87
+ this.light = document.createElement('a-light');
88
+ this.light.setAttribute('type', 'point');
89
+ this.light.setAttribute('color', this.data.color);
90
+ this.light.setAttribute('intensity', '0.75');
91
+ this.el.appendChild(this.light);
92
+ },
93
+ tick: function (time, timeDelta) {
94
+ var currentPos = this.el.getAttribute('position');
95
+ var speed = this.data.speed;
96
+ var bounds = this.data.bounds;
97
+ ['x', 'y', 'z'].forEach(axis => {
98
+ currentPos[axis] += speed[axis] * this.dir[axis] * (timeDelta / 1000);
99
+ if (Math.abs(currentPos[axis]) > bounds[axis]) {
100
+ this.dir[axis] *= -1;
101
+ }
102
+ });
103
+ this.el.setAttribute('position', currentPos);
104
+ }
105
+ });
106
+ function moveCamera(direction) {
107
+ var camera = document.querySelector('[camera]');
108
+ var rig = document.querySelector('#rig');
109
+ var pos = rig.getAttribute('position');
110
+ var rot = rig.getAttribute('rotation');
111
+ var speed = 0.5;
112
+ var rotationSpeed = 5;
113
+ switch(direction) {
114
+ case 'up': pos.y += speed; break;
115
+ case 'down': pos.y -= speed; break;
116
+ case 'forward': pos.z -= speed; break;
117
+ case 'left': pos.x -= speed; break;
118
+ case 'right': pos.x += speed; break;
119
+ case 'rotateLeft': rot.y += rotationSpeed; break;
120
+ case 'rotateRight': rot.y -= rotationSpeed; break;
121
+ case 'reset': pos = {x: 0, y: 10, z: 0}; rot = {x: -90, y: 0, z: 0}; break;
122
+ case 'ground': pos = {x: 0, y: 1.6, z: 0}; rot = {x: 0, y: 0, z: 0}; break;
123
+ }
124
+ rig.setAttribute('position', pos);
125
+ rig.setAttribute('rotation', rot);
126
  }
127
+ function fireRaycast() {
128
+ var camera = document.querySelector('[camera]');
129
+ var direction = new THREE.Vector3();
130
+ camera.object3D.getWorldDirection(direction);
131
+ var raycaster = new THREE.Raycaster();
132
+ raycaster.set(camera.object3D.position, direction);
133
+ var intersects = raycaster.intersectObjects(document.querySelectorAll('.raycastable').map(el => el.object3D), true);
134
+ if (intersects.length > 0) {
135
+ var hitObject = intersects[0].object.el;
136
+ if (hitObject.components.bouncing) {
137
+ hitObject.components.bouncing.boost();
138
+ score += 10;
139
+ document.getElementById('score').setAttribute('value', 'Score: ' + score);
140
+ }
141
+ }
142
+ }
143
+ document.addEventListener('keydown', function(event) {
144
+ switch(event.key.toLowerCase()) {
145
+ case 'w': moveCamera('up'); break;
146
+ case 's': moveCamera('forward'); break;
147
+ case 'x': moveCamera('down'); break;
148
+ case 'q': moveCamera('rotateLeft'); break;
149
+ case 'e': moveCamera('rotateRight'); break;
150
+ case 'z': moveCamera('reset'); break;
151
+ case 'c': moveCamera('ground'); break;
152
+ case ' ': fireRaycast(); break;
153
+ }
154
+ });
155
+ </script>
156
+ """
157
 
158
+ def create_aframe_entity(file_stem, file_type, position):
159
+ rotation = f"0 {random.uniform(0, 360)} 0"
160
+ bounce_speed = f"{random.uniform(0.05, 0.1)} {random.uniform(0.05, 0.1)} {random.uniform(0.05, 0.1)}"
161
+ bounce_dist = f"0.1 0.1 0.1"
162
+ if file_type == 'obj':
163
+ return f'<a-entity position="{position}" rotation="{rotation}" scale="0.5 0.5 0.5" obj-model="obj: #{file_stem}" class="raycastable" draggable bouncing="speed: {bounce_speed}; dist: {bounce_dist}"></a-entity>'
164
+ elif file_type == 'glb':
165
+ return f'<a-entity position="{position}" rotation="{rotation}" scale="0.5 0.5 0.5" gltf-model="#{file_stem}" class="raycastable" draggable bouncing="speed: {bounce_speed}; dist: {bounce_dist}"></a-entity>'
166
+ elif file_type in ['webp', 'png']:
167
+ return f'<a-image position="{position}" rotation="-90 0 0" src="#{file_stem}" width="0.5" height="0.5" class="raycastable" draggable bouncing="speed: {bounce_speed}; dist: {bounce_dist}"></a-image>'
168
+ elif file_type == 'mp4':
169
+ return f'<a-video position="{position}" rotation="-90 0 0" src="#{file_stem}" width="0.5" height="0.5" class="raycastable" draggable bouncing="speed: {bounce_speed}; dist: {bounce_dist}"></a-video>'
170
+ return ''
171
 
172
+ @st.cache_data
173
+ def encode_file(file_path):
174
+ with open(file_path, "rb") as file:
175
+ return base64.b64encode(file.read()).decode()
 
 
 
 
 
 
 
 
 
176
 
177
+ @st.cache_data
178
+ def generate_tilemap(files, directory, grid_width, grid_height, max_unique_models=5):
179
+ assets = "<a-assets>"
180
+ entities = ""
181
+ tile_size = 1
182
+ start_x = -(grid_width * tile_size) / 2
183
+ start_z = -(grid_height * tile_size) / 2
184
+ unique_files = random.sample(files, min(len(files), max_unique_models))
185
+ encoded_files = {}
186
+ for file in unique_files:
187
+ file_path = os.path.join(directory, file)
188
+ file_type = file.split('.')[-1]
189
+ encoded_file = encode_file(file_path)
190
+ encoded_files[file] = encoded_file
191
+ if file_type in ['obj', 'glb']:
192
+ assets += f'<a-asset-item id="{Path(file).stem}" src="data:application/octet-stream;base64,{encoded_file}"></a-asset-item>'
193
+ elif file_type in ['webp', 'png']:
194
+ assets += f'<img id="{Path(file).stem}" src="data:image/{file_type};base64,{encoded_file}">'
195
+ elif file_type == 'mp4':
196
+ assets += f'<video id="{Path(file).stem}" src="data:video/mp4;base64,{encoded_file}"></video>'
197
+ for i in range(grid_width):
198
+ for j in range(grid_height):
199
+ x = start_x + (i * tile_size)
200
+ z = start_z + (j * tile_size)
201
+ position = f"{x} 0 {z}"
202
+ if unique_files:
203
+ file = random.choice(unique_files)
204
+ file_type = file.split('.')[-1]
205
+ entities += create_aframe_entity(Path(file).stem, file_type, position)
206
+ assets += "</a-assets>"
207
+ return assets, entities
208
 
209
  def main():
210
+ st.set_page_config(layout="wide")
211
+ with st.sidebar:
212
+ st.markdown("### ๐Ÿค– 3D AI Using Claude 3.5 Sonnet for AI Pair Programming")
213
+ st.markdown("[Open 3D Animation Toolkit](https://huggingface.co/spaces/awacke1/3d_animation_toolkit)", unsafe_allow_html=True)
214
+ st.markdown("### โฌ†๏ธ Upload")
215
+ uploaded_files = st.file_uploader("Add files:", accept_multiple_files=True, key="file_uploader")
216
+ st.markdown("### ๐ŸŽฎ Camera Controls")
217
+ col1, col2, col3 = st.columns(3)
218
+ with col1:
219
+ st.button("โฌ…๏ธ", on_click=lambda: st.session_state.update({'camera_move': 'left'}))
220
+ st.button("๐Ÿ”„โ†บ", on_click=lambda: st.session_state.update({'camera_move': 'rotateLeft'}))
221
+ st.button("๐Ÿ”", on_click=lambda: st.session_state.update({'camera_move': 'reset'}))
222
+ with col2:
223
+ st.button("โฌ†๏ธ", on_click=lambda: st.session_state.update({'camera_move': 'up'}))
224
+ st.button("๐Ÿ‘€", on_click=lambda: st.session_state.update({'camera_move': 'ground'}))
225
+ st.button("๐Ÿ”ซ", on_click=lambda: st.session_state.update({'camera_move': 'fire'}))
226
+ with col3:
227
+ st.button("โžก๏ธ", on_click=lambda: st.session_state.update({'camera_move': 'right'}))
228
+ st.button("โฌ‡๏ธ", on_click=lambda: st.session_state.update({'camera_move': 'down'}))
229
+ st.button("โฉ", on_click=lambda: st.session_state.update({'camera_move': 'forward'}))
230
+ st.markdown("### ๐Ÿ—บ๏ธ Grid Size")
231
+ grid_width = st.slider("Grid Width", 1, 8, 8)
232
+ grid_height = st.slider("Grid Height", 1, 5, 5)
233
+ st.markdown("### โ„น๏ธ Instructions")
234
+ st.write("- W: Camera up\n- S: Move forward\n- X: Camera down\n- Q/E: Rotate camera left/right\n- Z: Reset camera to top view\n- C: Move camera to ground level\n- Spacebar: Fire raycast\n- Click and drag to move objects\n- Mouse wheel to zoom\n- Right-click and drag to rotate view")
235
+ st.markdown("### ๐Ÿ“ Directory")
236
+ directory = st.text_input("Enter path:", ".", key="directory_input")
237
+
238
+ if not os.path.isdir(directory):
239
+ st.sidebar.error("Invalid directory path")
240
  return
241
+ file_types = ['obj', 'glb', 'webp', 'png', 'mp4']
242
+ if uploaded_files:
243
+ for uploaded_file in uploaded_files:
244
+ file_extension = Path(uploaded_file.name).suffix.lower()[1:]
245
+ if file_extension in file_types:
246
+ with open(os.path.join(directory, uploaded_file.name), "wb") as f:
247
+ shutil.copyfileobj(uploaded_file, f)
248
+ st.sidebar.success(f"Uploaded: {uploaded_file.name}")
249
+ else:
250
+ st.sidebar.warning(f"Skipped unsupported file: {uploaded_file.name}")
251
+ files = [f for f in os.listdir(directory) if f.split('.')[-1] in file_types]
252
 
253
+ aframe_scene = f"""
254
+ <a-scene embedded style="height: 600px; width: 100%;">
255
+ <a-entity id="rig" position="0 {max(grid_width, grid_height)} 0" rotation="-90 0 0">
256
+ <a-camera fov="60" look-controls wasd-controls="enabled: false" cursor="rayOrigin: mouse" raycaster="objects: .raycastable"></a-camera>
257
+ </a-entity>
258
+ <a-sky color="#87CEEB"></a-sky>
259
+ <a-entity moving-light="color: #FFD700; speed: 0.07 0.05 0.06; bounds: 4 3 4" position="2 2 -2"></a-entity>
260
+ <a-entity moving-light="color: #FF6347; speed: 0.06 0.08 0.05; bounds: 4 3 4" position="-2 1 2"></a-entity>
261
+ <a-entity moving-light="color: #00CED1; speed: 0.05 0.06 0.07; bounds: 4 3 4" position="0 3 0"></a-entity>
262
+ <a-text id="score" value="Score: 0" position="-1.5 1 -2" scale="0.5 0.5 0.5" color="white"></a-text>
263
+ """
 
 
 
 
 
 
 
 
 
 
264
 
265
+ assets, entities = generate_tilemap(files, directory, grid_width, grid_height, max_unique_models=5)
266
+ aframe_scene += assets + entities + "</a-scene>"
267
 
268
+ camera_move = st.session_state.get('camera_move', None)
269
+ if camera_move:
270
+ if camera_move == 'fire':
271
+ aframe_scene += "<script>fireRaycast();</script>"
 
 
272
  else:
273
+ aframe_scene += f"<script>moveCamera('{camera_move}');</script>"
274
+ st.session_state.pop('camera_move')
 
 
 
 
 
 
 
275
 
276
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
277
+ # ADDITION: include the A-Frame extras loader so OBJ/GLTF actually render
278
+ loader_script = """
 
 
 
 
 
 
 
 
 
 
 
279
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/aframe-extras.loaders.min.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  """
281
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
282
 
283
+ st.components.v1.html(
284
+ load_aframe_and_extras()
285
+ + loader_script
286
+ + aframe_scene,
287
+ height=600
288
+ )
289
 
290
  if __name__ == "__main__":
291
  main()