File size: 17,763 Bytes
1d5ce9c
 
 
 
9c98a20
1d5ce9c
 
 
 
 
 
 
4322098
e961569
1d626f8
e961569
 
1d626f8
 
c22447e
9c98a20
4322098
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c22447e
9c98a20
1d5ce9c
 
c22447e
9c98a20
4322098
1d5ce9c
4322098
9612b9a
e961569
53c7232
 
d2956fc
c22447e
9c98a20
e961569
4322098
e961569
9612b9a
4322098
 
1abb14b
4322098
 
1abb14b
4322098
 
1abb14b
4322098
 
1abb14b
4322098
 
1abb14b
4322098
 
1abb14b
4322098
 
1abb14b
4322098
 
1abb14b
4322098
1d5ce9c
d2956fc
 
1d5ce9c
c22447e
9c98a20
c22447e
 
1d626f8
c22447e
 
1abb14b
1d626f8
 
 
 
 
1abb14b
 
1d626f8
 
 
 
 
1abb14b
1d626f8
 
1abb14b
1d626f8
 
1abb14b
badfc89
 
1abb14b
badfc89
 
1abb14b
c22447e
53c7232
 
1abb14b
53c7232
 
 
c22447e
 
1abb14b
 
 
 
 
 
c22447e
e961569
 
1d626f8
53c7232
c22447e
 
1d626f8
c22447e
 
9c98a20
e961569
 
 
1d626f8
 
e961569
1d626f8
e961569
1d626f8
 
 
 
 
e961569
1d626f8
e961569
 
 
 
 
9c98a20
1d5ce9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c22447e
9c98a20
1d5ce9c
 
e961569
4322098
 
 
 
 
 
 
 
1d626f8
 
badfc89
 
1d626f8
 
1abb14b
 
 
 
1d5ce9c
 
 
 
 
9c98a20
1d5ce9c
 
 
 
9c98a20
1d5ce9c
 
 
 
 
 
 
 
 
 
 
 
9c98a20
1d5ce9c
 
4322098
1d5ce9c
 
 
 
 
 
 
9c98a20
1d5ce9c
 
 
 
 
 
 
 
 
 
badfc89
1d5ce9c
badfc89
1d5ce9c
 
9c98a20
1d5ce9c
9c98a20
c22447e
 
1ad4ab4
 
 
 
badfc89
c22447e
badfc89
c22447e
 
 
 
 
 
 
 
 
 
1d5ce9c
 
 
 
c22447e
 
9c98a20
 
1d5ce9c
9c98a20
1d5ce9c
 
 
9c98a20
4322098
53c7232
4322098
53c7232
 
 
4322098
53c7232
 
 
4322098
53c7232
 
 
1d5ce9c
9c98a20
1d5ce9c
9c98a20
1ad4ab4
9c98a20
 
1d5ce9c
badfc89
1d5ce9c
 
 
 
9c98a20
 
 
 
 
1d5ce9c
9c98a20
1d5ce9c
9c98a20
1d5ce9c
9c98a20
1d5ce9c
 
9c98a20
4322098
1d5ce9c
 
 
 
 
 
9c98a20
 
1d5ce9c
9c98a20
1d5ce9c
 
 
 
 
9c98a20
1d5ce9c
4322098
 
 
1d5ce9c
 
 
 
 
 
 
 
 
 
 
 
9c98a20
 
 
1d5ce9c
 
9c98a20
e961569
c22447e
 
4322098
1d5ce9c
c22447e
 
 
 
 
1d5ce9c
 
 
 
4322098
1d5ce9c
 
9c98a20
1d5ce9c
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
import streamlit as st
import os, base64, shutil, random
from pathlib import Path

# ๐Ÿš€ 1. Load Aframe and Scripts: Let's get the party started with Aframe and friends! ๐ŸŽ‰
@st.cache_data
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>
    let score = 0;
    let selectedEntity = null;
    let velocity = {x: 0, y: 0, z: 0};
    let rotVelocity = {x: 0, y: 0, z: 0};
    let maxSpeed = 0.5;
    let accel = 0.02;
    let maxRotSpeed = 2;
    let rotAccel = 0.1;

    // ๐Ÿ–ฑ๏ธ 1.1 Draggable Component: Click and drag like a pro! ๐Ÿ‹๏ธ
    AFRAME.registerComponent('draggable', {
      init: function () {
        this.el.addEventListener('mousedown', this.onMouseDown.bind(this));
        this.el.addEventListener('touchstart', this.onTouchStart.bind(this));
        this.el.addEventListener('mouseup', this.onMouseUp.bind(this));
        this.el.addEventListener('touchend', this.onTouchEnd.bind(this));
        this.el.addEventListener('mousemove', this.onMouseMove.bind(this));
        this.el.addEventListener('touchmove', this.onTouchMove.bind(this));
      },
      onMouseDown: function (evt) {
        selectedEntity = this.el;
      },
      onTouchStart: function (evt) {
        evt.preventDefault();
        selectedEntity = this.el;
      },
      onMouseUp: function () {
        selectedEntity = null;
      },
      onTouchEnd: function () {
        selectedEntity = null;
      },
      onMouseMove: function (evt) {
        if (selectedEntity) {
          let cam = document.querySelector('[camera]');
          let dir = new THREE.Vector3();
          cam.object3D.getWorldDirection(dir);
          let rc = new THREE.Raycaster(cam.object3D.position, dir);
          let plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
          let pos = new THREE.Vector3();
          rc.ray.intersectPlane(plane, pos);
          selectedEntity.setAttribute('position', {x: pos.x, y: 0, z: pos.z});
        }
      },
      onTouchMove: function (evt) {
        evt.preventDefault();
        if (selectedEntity) {
          let cam = document.querySelector('[camera]');
          let dir = new THREE.Vector3();
          cam.object3D.getWorldDirection(dir);
          let rc = new THREE.Raycaster(cam.object3D.position, dir);
          let plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
          let pos = new THREE.Vector3();
          rc.ray.intersectPlane(plane, pos);
          selectedEntity.setAttribute('position', {x: pos.x, y: 0, z: pos.z});
        }
      }
    });

    // โ›น๏ธ 1.2 Bouncing and Lights: Keep the scene lively! โœจ
    AFRAME.registerComponent('bouncing', {/* โ€ฆ */});
    AFRAME.registerComponent('moving-light', {/* โ€ฆ */});

    // ๐Ÿ“ธ 1.3 Move Camera: Snap to your favorite quadrant views! ๐Ÿ–ผ๏ธ
    function moveCamera(view) {
      var rig = document.querySelector('#rig');
      var gw = 8, gh = 8;
      var center = {x: 0, y: 0, z: 0};
      var height = Math.max(gw, gh) * 1.5; // y=12 for center view
      var lowHeight = 3; // y=3 for closer views
      var quadrantOffset = Math.max(gw, gh) / 4; // 2 units to center on quadrants
      var pos, rot;

      rot = {x: -90, y: 0, z: 0}; // Always top-down view ๐Ÿ“‰

      switch(view) {
        case 'center':
          pos = {x: center.x, y: height, z: center.z};
          break;
        case 'left':
          pos = {x: -quadrantOffset, y: lowHeight, z: center.z};
          break;
        case 'right':
          pos = {x: quadrantOffset, y: lowHeight, z: center.z};
          break;
        case 'front':
          pos = {x: center.x, y: lowHeight, z: -quadrantOffset};
          break;
        case 'back':
          pos = {x: center.x, y: lowHeight, z: quadrantOffset};
          break;
        case 'angle1':
          pos = {x: -quadrantOffset, y: lowHeight, z: -quadrantOffset};
          break;
        case 'angle2':
          pos = {x: quadrantOffset, y: lowHeight, z: -quadrantOffset};
          break;
        case 'angle3':
          pos = {x: -quadrantOffset, y: lowHeight, z: quadrantOffset};
          break;
        case 'angle4':
          pos = {x: quadrantOffset, y: lowHeight, z: quadrantOffset};
          break;
      }
      rig.setAttribute('position', pos);
      rig.setAttribute('rotation', rot);
    }

    // โœˆ๏ธ 1.4 Fly Camera: Soar through the scene with style! ๐Ÿ›ฉ๏ธ
    function flyCamera(action) {
      var rig = document.querySelector('#rig');
      var pos = rig.getAttribute('position');

      switch(action) {
        case 'moveLeft':
          var leftDir = new THREE.Vector3(-1, 0, 0);
          leftDir.applyQuaternion(rig.object3D.quaternion);
          pos.x += leftDir.x * 0.1;
          pos.y += leftDir.y * 0.1;
          pos.z += leftDir.z * 0.1;
          break;
        case 'moveRight':
          var rightDir = new THREE.Vector3(1, 0, 0);
          rightDir.applyQuaternion(rig.object3D.quaternion);
          pos.x += rightDir.x * 0.1;
          pos.y += rightDir.y * 0.1;
          pos.z += rightDir.z * 0.1;
          break;
        case 'rotateCCW':
          rotVelocity.y = Math.min(rotVelocity.y + rotAccel, maxRotSpeed);
          break;
        case 'rotateCW':
          rotVelocity.y = Math.max(rotVelocity.y - rotAccel, -maxRotSpeed);
          break;
        case 'pitchUp':
          rotVelocity.x = Math.max(rotVelocity.x - rotAccel, -maxRotSpeed);
          break;
        case 'pitchDown':
          rotVelocity.x = Math.min(rotVelocity.x + rotAccel, maxRotSpeed);
          break;
        case 'forward':
          var cam = document.querySelector('[camera]');
          var dir = new THREE.Vector3();
          cam.object3D.getWorldDirection(dir);
          pos.x += dir.x * 0.1;
          pos.y += dir.y * 0.1;
          pos.z += dir.z * 0.1;
          break;
        case 'backward':
          var cam = document.querySelector('[camera]');
          var dir = new THREE.Vector3();
          cam.object3D.getWorldDirection(dir);
          pos.x -= dir.x * 0.1;
          pos.y -= dir.y * 0.1;
          pos.z -= dir.z * 0.1;
          break;
        case 'stop':
          velocity = {x: 0, y: 0, z: 0};
          rotVelocity = {x: 0, y: 0, z: 0};
          moveCamera('center');
          break;
      }
      rig.setAttribute('position', pos);
    }

    // ๐Ÿ”„ 1.5 Update Camera: Keep the camera flying smoothly! ๐Ÿ•Š๏ธ
    function updateCameraPosition() {
      var rig = document.querySelector('#rig');
      var pos = rig.getAttribute('position');
      var rot = rig.getAttribute('rotation');

      pos.x += velocity.x;
      pos.y = Math.max(1, pos.y + velocity.y);
      pos.z += velocity.z;

      rot.x += rotVelocity.x;
      rot.y += rotVelocity.y;
      rot.z += rotVelocity.z;

      rig.setAttribute('position', pos);
      rig.setAttribute('rotation', rot);
      requestAnimationFrame(updateCameraPosition);
    }

    requestAnimationFrame(updateCameraPosition);

    // ๐ŸŽฏ 1.6 Fire Raycast: Zap objects with a laser beam! โšก
    function fireRaycast() {
      var cam = document.querySelector('[camera]');
      var dir = new THREE.Vector3(); cam.object3D.getWorldDirection(dir);
      var rc = new THREE.Raycaster(cam.object3D.position, dir);
      var hits = rc.intersectObjects(
        Array.from(document.querySelectorAll('.raycastable')).map(e=>e.object3D), true
      );
      if (hits.length>0) {
        var el = hits[0].object.el;
        if (el.components.bouncing) {
          el.components.bouncing.boost();
          score += 10;
          document.getElementById('score').setAttribute('value','Score: '+score);
        }
      }
    }

    // โŒจ๏ธ 1.7 Keyboard Listener: Take control with your keys! ๐ŸŽฎ
    document.addEventListener('keydown', e => {
      switch(e.key.toLowerCase()){
        case '1': moveCamera('center'); break;
        case '2': moveCamera('left');   break;
        case '3': moveCamera('right');  break;
        case '4': moveCamera('front');  break;
        case '5': moveCamera('back');   break;
        case '6': moveCamera('angle1'); break;
        case '7': moveCamera('angle2'); break;
        case '8': moveCamera('angle3'); break;
        case '9': moveCamera('angle4'); break;
        case 'q': flyCamera('rotateCCW'); break;
        case 'e': flyCamera('rotateCW');  break;
        case 'z': flyCamera('pitchUp');   break;
        case 'c': flyCamera('pitchDown'); break;
        case 'a': flyCamera('moveLeft');  break;
        case 'd': flyCamera('moveRight'); break;
        case 'w': flyCamera('forward');   break;
        case 'x': flyCamera('backward');  break;
        case 's': flyCamera('stop');      break;
        case ' ': fireRaycast();          break;
      }
    });
    </script>
    """

# ๐Ÿ” 2. Encode File: Sneakily encode files into base64! ๐Ÿ•ต๏ธ
@st.cache_data
def encode_file(path):
    with open(path,'rb') as f: return base64.b64encode(f.read()).decode()

# ๐Ÿฐ 3. Create Aframe Entity: Build magical 3D models! ๐Ÿช„
def create_aframe_entity(stem, ext, pos):
    ry = random.uniform(0,360)
    if ext == 'obj':
        return (f'<a-entity obj-model="obj:#{stem}" '
                f'position="{pos}" rotation="0 {ry} 0" scale="1 1 1" '
                'class="raycastable" draggable></a-entity>')
    if ext in ('glb','gltf'):
        return (f'<a-entity gltf-model="#{stem}" '
                f'position="{pos}" rotation="0 {ry} 0" scale="1 1 1" '
                'class="raycastable" draggable></a-entity>')
    return ''

# ๐Ÿ—บ๏ธ 4. Generate Tilemap: Craft the world one tile at a time! ๐ŸŒ
@st.cache_data
def generate_tilemap(files, dirpath, gw=8, gh=8):
    img_exts   = ['webp','png','jpeg','jpg']
    model_exts = ['obj','glb','gltf']
    vid_exts   = ['mp4']

    imgs   = [f for f in files if f.split('.')[-1] in img_exts]
    models = [f for f in files if f.split('.')[-1] in model_exts]
    vids   = [f for f in files if f.split('.')[-1] in vid_exts]

    # ๐Ÿ“ฆ 4.1 Load Assets: Pack up all the goodies! ๐ŸŽ
    assets = "<a-assets>"
    for f in files:
        stem = Path(f).stem; ext=f.split('.')[-1]
        data=encode_file(os.path.join(dirpath,f))
        if ext in model_exts:
            assets += (f'<a-asset-item id="{stem}" '
                       f'src="data:application/octet-stream;base64,{data}"></a-asset-item>')
        elif ext in img_exts:
            assets += f'<img id="{stem}" src="data:image/{ext};base64,{data}">'
        elif ext in vid_exts:
            assets += (f'<video id="{stem}" preload="auto" '
                       f'src="data:video/mp4;base64,{data}" '
                       'loop="true" autoplay="true" muted="true" playsinline webkit-playsinline></video>')
    assets += "</a-assets>"

    # ๐Ÿž๏ธ 4.2 Build the Scene: Lay out the tiles and models! ๐Ÿก
    entities = ""
    model_counts = {f: 0 for f in models}  # Track spawned model counts ๐Ÿ“Š
    if vids:
        v = vids[0]; s = Path(v).stem
        # ๐Ÿ“น Adjust video dimensions to cover the tilemap while preserving aspect ratio ๐ŸŽฅ
        # Assume a common video aspect ratio (16:9) and scale to fit 8x8 tilemap
        videoWidth = 8  # Match tilemap width
        videoHeight = 8  # Match tilemap height (square to fit tilemap)
        entities += (f'<a-video src="#{s}" width="{videoWidth}" height="{videoHeight}" '
                     f'rotation="-90 0 0" position="0 0.05 0" '
                     'material="shader:flat; src:#{s}" loop="true" autoplay="true"></a-video>')
    else:
        sx = -gw/2; sz = -gh/2
        for i in range(gw):
          for j in range(gh):
            x,y,z = sx+i,0,sz+j
            if imgs:
              img = random.choice(imgs); s = Path(img).stem
              entities += (f'<a-plane src="#{s}" width="1" height="1" '
                           f'rotation="-90 0 0" position="{x} 0 {z}"></a-plane>')
    sx = -gw/2; sz = -gh/2
    for i in range(gw):
      for j in range(gh):
        x,y,z = sx+i,0,sz+j
        if models:
          m = random.choice(models); ext = m.split('.')[-1]; s = Path(m).stem
          entities += create_aframe_entity(s, ext, f"{x} 0 {z}")
          model_counts[m] += 1  # Increment count for this model ๐Ÿ“ˆ
    return assets, entities, model_counts

# ๐ŸŽจ 5. Main Function: Paint the canvas of your 3D world! ๐Ÿ–Œ๏ธ
def main():
    st.set_page_config(layout="wide")
    with st.sidebar:
        # ๐Ÿงญ 5.1 Camera Views UI: Pick your perfect angle! ๐Ÿ“
        st.markdown("### ๐Ÿงญ Camera Views")
        st.markdown("**Select Quadrant** ๐Ÿ“ท")
        cols = st.columns(3)
        cols[0].button("โ†–๏ธ Top-Left", on_click=lambda: st.session_state.update({'camera_view': 'angle1'}))
        cols[1].button("โฌ†๏ธ Top", on_click=lambda: st.session_state.update({'camera_view': 'front'}))
        cols[2].button("โ†—๏ธ Top-Right", on_click=lambda: st.session_state.update({'camera_view': 'angle2'}))
        cols = st.columns(3)
        cols[0].button("โฌ…๏ธ Left", on_click=lambda: st.session_state.update({'camera_view': 'left'}))
        cols[1].button("๐Ÿ”ณ Center", on_click=lambda: st.session_state.update({'camera_view': 'center'}))
        cols[2].button("โžก๏ธ Right", on_click=lambda: st.session_state.update({'camera_view': 'right'}))
        cols = st.columns(3)
        cols[0].button("โ†™๏ธ Bottom-Left", on_click=lambda: st.session_state.update({'camera_view': 'angle3'}))
        cols[1].button("โฌ‡๏ธ Bottom", on_click=lambda: st.session_state.update({'camera_view': 'back'}))
        cols[2].button("โ†˜๏ธ Bottom-Right", on_click=lambda: st.session_state.update({'camera_view': 'angle4'}))

        # ๐Ÿ“‚ 5.2 File Uploader: Drop your media treasures here! ๐Ÿ’Ž
        st.markdown("### โž• Add Media Files")
        ups = st.file_uploader(
            "๐Ÿ–ผ๏ธ png/jpeg, ๐Ÿฐ obj/glb/gltf, ๐Ÿ“น mp4:",
            accept_multiple_files=True
        )
        st.markdown("### ๐Ÿ“‹ Uploaded Model Files")
        directory = st.text_input("", ".", key="dir", placeholder="Enter directory path (e.g., '.')")
        if os.path.isdir(directory):
            files = [f for f in os.listdir(directory) if f.split('.')[-1] in ['obj', 'glb', 'gltf']]
            if files:
                for i, f in enumerate(files, 1):
                    # Add emoji based on file type and show spawn count ๐Ÿ–Œ๏ธ
                    ext = f.split('.')[-1]
                    emoji = "๐Ÿฐ" if ext == 'obj' else "๐Ÿฏ"  # ๐Ÿฐ for obj, ๐Ÿฏ for glb/gltf
                    count = st.session_state.get('model_counts', {}).get(f, 0)
                    st.markdown(f"{i}. {emoji} {f} (Spawned: {count})")
            else:
                st.markdown("No model files found. ๐Ÿ•ธ๏ธ")

    # ๐Ÿšจ 5.3 Directory Check: Make sure the path exists! ๐Ÿ›ค๏ธ
    if not os.path.isdir(directory):
        st.sidebar.error("Invalid directory ๐Ÿšซ")
        return

    # ๐Ÿ“ฅ 5.4 Handle Uploads: Bring in the new files! ๐Ÿ“ฆ
    types = ['obj','glb','gltf','webp','png','jpeg','jpg','mp4']
    if ups:
      for up in ups:
        ext=Path(up.name).suffix.lower()[1:]
        if ext in types:
          with open(os.path.join(directory,up.name),'wb') as f:
            shutil.copyfileobj(up,f)
          emoji = "๐Ÿ–ผ๏ธ" if ext in ['webp', 'png', 'jpeg', 'jpg'] else "๐Ÿฐ" if ext in ['obj', 'glb', 'gltf'] else "๐Ÿ“น"
          st.sidebar.success(f"Uploaded {emoji} {up.name}")
        else:
          st.sidebar.warning(f"Skipped {up.name} ๐Ÿšง")

    files = [f for f in os.listdir(directory) if f.split('.')[-1] in types]

    spot_h = max(8,8)*1.5

    # ๐ŸŒ 5.5 Build the Scene: Create your 3D masterpiece! ๐ŸŽฌ
    scene = f"""
    <a-scene embedded style="height:100vh; width:100vw; position:fixed; top:0; left:0;">
      <a-entity id="rig" position="0 12 0" rotation="-90 0 0">
        <a-camera fov="60" look-controls wasd-controls="enabled:false"
                  cursor="rayOrigin:mouse" raycaster="objects:.raycastable">
        </a-camera>
      </a-entity>
      <a-sky color="#87CEEB"></a-sky>
      <a-entity moving-light="color:#FFD700; speed:0.07 0.05 0.06; bounds:4 3 4" position="2 2 -2"></a-entity>
      <a-entity moving-light="color:#FF6347; speed:0.06 0.08 0.05; bounds:4 3 4" position="-2 1 2"></a-entity>
      <a-entity moving-light="color:#00CED1; speed:0.05 0.06 0.07; bounds:4 3 4" position="0 3 0"></a-entity>
      <a-entity light="type:spot; color:#FFF; intensity:1; angle:45; penumbra:0.2"
                position="0 {spot_h} 0" rotation="-90 0 0"></a-entity>
      <a-text id="score" value="Score:0" position="-1.5 2 -3" scale="0.5 0.5 0.5" color="white"></a-text>
    """

    # ๐Ÿ—๏ธ 5.6 Add Assets and Entities: Fill the world with wonders! ๐ŸŒŸ
    assets, ents, model_counts = generate_tilemap(files, directory, 8, 8)
    st.session_state['model_counts'] = model_counts  # Store counts for display ๐Ÿ“Š
    scene += assets + ents + "</a-scene>"

    # ๐ŸŽฅ 5.7 Apply Camera Actions: Lights, camera, action! ๐ŸŽฌ
    view_cmd = st.session_state.get('camera_view', 'center')
    if view_cmd:
      scene += f"<script>moveCamera('{view_cmd}');</script>"
      st.session_state.pop('camera_view', None)

    fly_cmd = st.session_state.get('fly_action')
    if fly_cmd:
      scene += f"<script>flyCamera('{fly_cmd}');</script>"
      st.session_state.pop('fly_action', None)

    loader = '<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/aframe-extras.loaders.min.js"></script>'

    st.components.v1.html(
      load_aframe_and_extras() + loader + scene,
      height=1000
    )

# ๐Ÿ 6. Run the Show: Letโ€™s launch this 3D adventure! ๐ŸŽญ
if __name__ == "__main__":
    main()