awacke1 commited on
Commit
b8df290
·
verified ·
1 Parent(s): 750e754

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +227 -84
app.py CHANGED
@@ -4,25 +4,147 @@ from pathlib import Path
4
 
5
  @st.cache_data
6
  def load_aframe_and_extras():
7
- # ↔️ This is your original A-Frame + components + moveCamera/fireRaycast boilerplate,
8
- # with just the keydown mapping at the bottom tweaked to:
9
- # W → down
10
- # S → reset
11
- # X → up
12
  return """
13
  <script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
14
  <script src="https://unpkg.com/[email protected]/dist/aframe-event-set-component.min.js"></script>
15
  <script>
16
  let score = 0;
17
- AFRAME.registerComponent('draggable', { /* … your original code … */ });
18
- AFRAME.registerComponent('bouncing', { /* … your original code … */ });
19
- AFRAME.registerComponent('moving-light', { /* … your original code … */ });
20
 
21
- function moveCamera(direction) { /* … your original code … */ }
22
- function fireRaycast() { /* … your original code … */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- // —— Modified keys here ——
25
- document.addEventListener('keydown', function(event) {
26
  switch(event.key.toLowerCase()) {
27
  case 'w': moveCamera('down'); break;
28
  case 's': moveCamera('reset'); break;
@@ -37,98 +159,92 @@ def load_aframe_and_extras():
37
  </script>
38
  """
39
 
 
40
  def encode_file(file_path):
41
  with open(file_path, "rb") as f:
42
  return base64.b64encode(f.read()).decode()
43
 
44
  def create_aframe_entity(stem, file_type, position):
45
- """Only 3D models spin in place, scale=1, no bouncing/draggable."""
 
46
  if file_type == 'obj':
47
  return (
48
  f'<a-entity obj-model="obj: #{stem}" '
49
  f'position="{position}" rotation="0 0 0" scale="1 1 1" '
50
- f'animation="property: rotation; to: 0 360 0; loop: true; dur: 20000; easing: linear">'
51
- '</a-entity>'
52
  )
53
  if file_type in ('glb','gltf'):
54
  return (
55
  f'<a-entity gltf-model="#{stem}" '
56
  f'position="{position}" rotation="0 0 0" scale="1 1 1" '
57
- f'animation="property: rotation; to: 0 360 0; loop: true; dur: 20000; easing: linear">'
58
- '</a-entity>'
59
  )
60
- return "" # images/videos handled separately
61
 
62
  @st.cache_data
63
  def generate_tilemap(files, directory, gw, gh):
64
- """
65
- - Encodes *all* your files into <a-assets>.
66
- - Then for each cell:
67
- 1. Chooses a random PNG/WEBP → flat a-plane (ground).
68
- 2. Chooses a random OBJ/GLB → spinning 3D entity above.
69
- 3. If any MP4, chooses one → looping video plane just above ground.
70
- """
71
- img_exts = ('webp','png')
72
- model_exts = ('obj','glb','gltf')
73
- video_exts = ('mp4',)
74
 
75
  img_files = [f for f in files if f.split('.')[-1] in img_exts]
76
  model_files = [f for f in files if f.split('.')[-1] in model_exts]
77
- vid_files = [f for f in files if f.split('.')[-1] in video_exts]
78
 
79
- # --- Build assets ---
80
  assets = "<a-assets>"
81
  for f in files:
82
  stem = Path(f).stem
83
  ext = f.split('.')[-1]
84
- data = encode_file(os.path.join(directory,f))
85
  if ext in model_exts:
86
- assets += f'<a-asset-item id="{stem}" src="data:application/octet-stream;base64,{data}"></a-asset-item>'
 
 
 
 
87
  elif ext in img_exts:
88
  assets += f'<img id="{stem}" src="data:image/{ext};base64,{data}">'
89
- elif ext in video_exts:
90
  assets += (
91
- f'<video id="{stem}" src="data:video/mp4;base64,{data}" '
 
92
  'loop="true" autoplay="true" muted="true"></video>'
93
  )
94
  assets += "</a-assets>"
95
 
96
- # --- Spawn per-cell entities ---
97
  entities = ""
98
- tile = 1
99
- sx = -(gw*tile)/2
100
- sz = -(gh*tile)/2
101
 
102
  for i in range(gw):
103
  for j in range(gh):
104
- x = sx + i*tile
105
- z = sz + j*tile
106
 
107
- # 1) image-ground
108
  if img_files:
109
- img = random.choice(img_files)
110
- stem = Path(img).stem
111
  entities += (
112
- f'<a-plane src="#{stem}" '
113
- f'width="{tile}" height="{tile}" '
114
  f'rotation="-90 0 0" position="{x} 0.01 {z}"></a-plane>'
115
  )
116
 
117
  # 2) spinning 3D model
118
  if model_files:
119
- mdl = random.choice(model_files)
120
  ext = mdl.split('.')[-1]
121
- stem = Path(mdl).stem
122
  entities += create_aframe_entity(stem, ext, f"{x} 0.5 {z}")
123
 
124
- # 3) video-layer (optional)
125
  if vid_files:
126
- vid = random.choice(vid_files)
127
  stem = Path(vid).stem
128
  entities += (
129
- f'<a-video src="#{stem}" '
130
- f'width="{tile}" height="{tile}" '
131
- f'rotation="-90 0 0" position="{x} 0.2 {z}" autoplay loop muted></a-video>'
132
  )
133
 
134
  return assets, entities
@@ -136,35 +252,59 @@ def generate_tilemap(files, directory, gw, gh):
136
  def main():
137
  st.set_page_config(layout="wide")
138
  with st.sidebar:
139
- st.markdown("### 🤖 3D AI ")
140
- uploaded = st.file_uploader("Add files:", accept_multiple_files=True)
141
- st.markdown("### 🎮 Camera")
142
- cols = st.columns(3)
143
- for btn,dir in zip(("⬅️","🔄↺","🔝"),("left","rotateLeft","reset")):
144
- cols[0 if btn in ("⬅️","🔄↺","🔝") else 1 if ... else 2].button(
145
- btn, on_click=lambda d=dir: st.session_state.update({'camera_move':d})
146
- )
147
- # … your original camera buttons …
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  st.markdown("### 🗺️ Grid Size")
149
- gw = st.slider("Width", 1, 8, 8)
150
- gh = st.slider("Height",1, 5, 5)
151
- st.markdown("### 📁 Dir")
152
- directory = st.text_input("Path:", ".", key="dir")
 
153
 
154
  if not os.path.isdir(directory):
155
- st.sidebar.error("Invalid directory")
156
  return
157
 
158
- types = ('obj','glb','gltf','webp','png','mp4')
159
- files = [f for f in os.listdir(directory) if f.split('.')[-1] in types]
 
 
 
 
 
 
 
 
 
 
160
 
161
- # Build the <a-scene> shell
162
- scene = f"""
163
  <a-scene embedded style="height:600px; width:100%;">
164
- <a-entity id="rig" position="0 {max(gw,gh)} 0" rotation="-90 0 0">
165
- <a-camera fov="60" look-controls wasd-controls="enabled:false"
166
- cursor="rayOrigin:mouse" raycaster="objects:.raycastable">
167
- </a-camera>
168
  </a-entity>
169
  <a-sky color="#87CEEB"></a-sky>
170
  <a-entity moving-light="color:#FFD700; speed:0.07 0.05 0.06; bounds:4 3 4" position="2 2 -2"></a-entity>
@@ -173,21 +313,24 @@ def main():
173
  <a-text id="score" value="Score: 0" position="-1.5 1 -2" scale="0.5 0.5 0.5" color="white"></a-text>
174
  """
175
 
176
- assets, entities = generate_tilemap(files, directory, gw, gh)
177
- scene += assets + entities + "</a-scene>"
178
 
179
- # camera_move injection
180
- cm = st.session_state.get('camera_move', None)
181
- if cm:
182
- scene += f"<script>moveCamera('{cm}');</script>"
 
 
 
183
  st.session_state.pop('camera_move')
184
 
185
- # ────────────────────────────────────────────────────────────────────────────
186
- # Make sure OBJ/GLB loader is present *after* everything else
187
  loader = '<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/aframe-extras.loaders.min.js"></script>'
 
188
  st.components.v1.html(
189
- load_aframe_and_extras() + loader + scene,
190
- height=620
191
  )
192
 
193
  if __name__ == "__main__":
 
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/[email protected]/dist/aframe-event-set-component.min.js"></script>
10
  <script>
11
  let score = 0;
 
 
 
12
 
13
+ AFRAME.registerComponent('draggable', {
14
+ init: function () {
15
+ this.el.setAttribute('class', 'raycastable');
16
+ this.el.setAttribute('cursor-listener', '');
17
+ this.dragHandler = this.dragMove.bind(this);
18
+ this.el.sceneEl.addEventListener('mousemove', this.dragHandler);
19
+ this.el.addEventListener('mousedown', this.onDragStart.bind(this));
20
+ this.el.addEventListener('mouseup', this.onDragEnd.bind(this));
21
+ this.camera = document.querySelector('[camera]');
22
+ },
23
+ remove: function () {
24
+ this.el.removeAttribute('cursor-listener');
25
+ this.el.sceneEl.removeEventListener('mousemove', this.dragHandler);
26
+ },
27
+ onDragStart: function (evt) {
28
+ this.isDragging = true;
29
+ this.el.emit('dragstart');
30
+ },
31
+ onDragEnd: function (evt) {
32
+ this.isDragging = false;
33
+ this.el.emit('dragend');
34
+ },
35
+ dragMove: function (evt) {
36
+ if (!this.isDragging) return;
37
+ var camera = this.camera;
38
+ var vector = new THREE.Vector3(
39
+ evt.clientX / window.innerWidth * 2 - 1,
40
+ -(evt.clientY / window.innerHeight) * 2 + 1,
41
+ 0.5
42
+ );
43
+ vector.unproject(camera);
44
+ var dir = vector.sub(camera.position).normalize();
45
+ var distance = -camera.position.y / dir.y;
46
+ var pos = camera.position.clone().add(dir.multiplyScalar(distance));
47
+ this.el.setAttribute('position', pos);
48
+ }
49
+ });
50
+
51
+ AFRAME.registerComponent('bouncing', {
52
+ schema: {
53
+ speed: {type: 'vec3', default: {x: 0.1, y: 0.1, z: 0.1}},
54
+ dist: {type: 'vec3', default: {x: 0.5, y: 0.5, z: 0.5}}
55
+ },
56
+ init: function () {
57
+ this.originalPos = this.el.getAttribute('position');
58
+ this.dir = {x:1,y:1,z:1};
59
+ },
60
+ tick: function (time, timeDelta) {
61
+ var p = this.el.getAttribute('position');
62
+ var s = this.data.speed, d = this.data.dist;
63
+ ['x','y','z'].forEach(a => {
64
+ p[a] += s[a]*this.dir[a]*(timeDelta/1000);
65
+ if (Math.abs(p[a] - this.originalPos[a]) > d[a]) {
66
+ this.dir[a] *= -1;
67
+ }
68
+ });
69
+ this.el.setAttribute('position', p);
70
+ },
71
+ boost: function() {
72
+ var s = this.data.speed;
73
+ ['x','y','z'].forEach(a => s[a]*=1.5);
74
+ this.data.speed = s;
75
+ this.dir = { x:Math.random()>0.5?1:-1,
76
+ y:Math.random()>0.5?1:-1,
77
+ z:Math.random()>0.5?1:-1 };
78
+ }
79
+ });
80
+
81
+ AFRAME.registerComponent('moving-light', {
82
+ schema: {
83
+ color: {type:'color', default:'#FFF'},
84
+ speed: {type:'vec3', default:{x:0.1,y:0.1,z:0.1}},
85
+ bounds:{type:'vec3', default:{x:5,y:5,z:5}}
86
+ },
87
+ init: function(){
88
+ this.dir = {x:1,y:1,z:1};
89
+ this.light = document.createElement('a-light');
90
+ this.light.setAttribute('type','point');
91
+ this.light.setAttribute('color', this.data.color);
92
+ this.light.setAttribute('intensity','0.75');
93
+ this.el.appendChild(this.light);
94
+ },
95
+ tick: function(time, dt){
96
+ var p = this.el.getAttribute('position'),
97
+ s = this.data.speed,
98
+ b = this.data.bounds;
99
+ ['x','y','z'].forEach(a=>{
100
+ p[a] += s[a]*this.dir[a]*(dt/1000);
101
+ if (Math.abs(p[a])>b[a]) this.dir[a]*=-1;
102
+ });
103
+ this.el.setAttribute('position', p);
104
+ }
105
+ });
106
+
107
+ function moveCamera(direction) {
108
+ var rig = document.querySelector('#rig');
109
+ var pos = rig.getAttribute('position');
110
+ var rot = rig.getAttribute('rotation');
111
+ var speed = 0.5, rSpeed = 5;
112
+ switch(direction) {
113
+ case 'up': pos.y += speed; break;
114
+ case 'down': pos.y -= speed; break;
115
+ case 'forward': pos.z -= speed; break;
116
+ case 'left': pos.x -= speed; break;
117
+ case 'right': pos.x += speed; break;
118
+ case 'rotateLeft': rot.y += rSpeed; break;
119
+ case 'rotateRight': rot.y -= rSpeed; break;
120
+ case 'reset': pos = {x:0,y:10,z:0}; rot = {x:-90,y:0,z:0}; break;
121
+ case 'ground': pos = {x:0,y:1.6,z:0}; rot = {x:0,y:0,z:0}; break;
122
+ }
123
+ rig.setAttribute('position', pos);
124
+ rig.setAttribute('rotation', rot);
125
+ }
126
+
127
+ function fireRaycast() {
128
+ var camera = document.querySelector('[camera]'),
129
+ dir = new THREE.Vector3();
130
+ camera.object3D.getWorldDirection(dir);
131
+ var rc = new THREE.Raycaster();
132
+ rc.set(camera.object3D.position, dir);
133
+ var hits = rc.intersectObjects(
134
+ document.querySelectorAll('.raycastable').map(e=>e.object3D), true
135
+ );
136
+ if (hits.length>0) {
137
+ var el = hits[0].object.el;
138
+ if (el.components.bouncing) {
139
+ el.components.bouncing.boost();
140
+ score += 10;
141
+ document.getElementById('score').setAttribute('value','Score: '+score);
142
+ }
143
+ }
144
+ }
145
 
146
+ // —— Key remap: W→down, S→reset, X→up ——
147
+ document.addEventListener('keydown', function(event){
148
  switch(event.key.toLowerCase()) {
149
  case 'w': moveCamera('down'); break;
150
  case 's': moveCamera('reset'); break;
 
159
  </script>
160
  """
161
 
162
+ @st.cache_data
163
  def encode_file(file_path):
164
  with open(file_path, "rb") as f:
165
  return base64.b64encode(f.read()).decode()
166
 
167
  def create_aframe_entity(stem, file_type, position):
168
+ """1×1×1 scale, spin around Y, draggable but no bouncing."""
169
+ anim = 'animation="property: rotation; to: 0 360 0; loop: true; dur: 20000; easing: linear"'
170
  if file_type == 'obj':
171
  return (
172
  f'<a-entity obj-model="obj: #{stem}" '
173
  f'position="{position}" rotation="0 0 0" scale="1 1 1" '
174
+ f'class="raycastable" draggable {anim}></a-entity>'
 
175
  )
176
  if file_type in ('glb','gltf'):
177
  return (
178
  f'<a-entity gltf-model="#{stem}" '
179
  f'position="{position}" rotation="0 0 0" scale="1 1 1" '
180
+ f'class="raycastable" draggable {anim}></a-entity>'
 
181
  )
182
+ return ""
183
 
184
  @st.cache_data
185
  def generate_tilemap(files, directory, gw, gh):
186
+ img_exts = ['webp','png']
187
+ model_exts = ['obj','glb','gltf']
188
+ vid_exts = ['mp4']
 
 
 
 
 
 
 
189
 
190
  img_files = [f for f in files if f.split('.')[-1] in img_exts]
191
  model_files = [f for f in files if f.split('.')[-1] in model_exts]
192
+ vid_files = [f for f in files if f.split('.')[-1] in vid_exts]
193
 
 
194
  assets = "<a-assets>"
195
  for f in files:
196
  stem = Path(f).stem
197
  ext = f.split('.')[-1]
198
+ data = encode_file(os.path.join(directory, f))
199
  if ext in model_exts:
200
+ assets += (
201
+ f'<a-asset-item id="{stem}" '
202
+ f'src="data:application/octet-stream;base64,{data}">'
203
+ '</a-asset-item>'
204
+ )
205
  elif ext in img_exts:
206
  assets += f'<img id="{stem}" src="data:image/{ext};base64,{data}">'
207
+ elif ext in vid_exts:
208
  assets += (
209
+ f'<video id="{stem}" '
210
+ f'src="data:video/mp4;base64,{data}" '
211
  'loop="true" autoplay="true" muted="true"></video>'
212
  )
213
  assets += "</a-assets>"
214
 
 
215
  entities = ""
216
+ sx = -gw/2
217
+ sz = -gh/2
 
218
 
219
  for i in range(gw):
220
  for j in range(gh):
221
+ x = sx + i
222
+ z = sz + j
223
 
224
+ # 1) ground image
225
  if img_files:
226
+ img = img_files[(i*gh + j) % len(img_files)]
227
+ stem=Path(img).stem
228
  entities += (
229
+ f'<a-plane src="#{stem}" width="1" height="1" '
 
230
  f'rotation="-90 0 0" position="{x} 0.01 {z}"></a-plane>'
231
  )
232
 
233
  # 2) spinning 3D model
234
  if model_files:
235
+ mdl = model_files[(i*gh + j) % len(model_files)]
236
  ext = mdl.split('.')[-1]
237
+ stem=Path(mdl).stem
238
  entities += create_aframe_entity(stem, ext, f"{x} 0.5 {z}")
239
 
240
+ # 3) video layer
241
  if vid_files:
242
+ vid = vid_files[(i*gh + j) % len(vid_files)]
243
  stem = Path(vid).stem
244
  entities += (
245
+ f'<a-video src="#{stem}" width="1" height="1" '
246
+ f'rotation="-90 0 0" position="{x} 0.2 {z}" '
247
+ 'class="raycastable" draggable></a-video>'
248
  )
249
 
250
  return assets, entities
 
252
  def main():
253
  st.set_page_config(layout="wide")
254
  with st.sidebar:
255
+ st.markdown("### 🤖 3D AI Using Claude 3.5 Sonnet for AI Pair Programming")
256
+ st.markdown(
257
+ "[Open 3D Animation Toolkit]"
258
+ "(https://huggingface.co/spaces/awacke1/3d_animation_toolkit)",
259
+ unsafe_allow_html=True
260
+ )
261
+ st.markdown("### ⬆️ Upload")
262
+ uploaded_files = st.file_uploader("Add files:", accept_multiple_files=True, key="file_uploader")
263
+
264
+ st.markdown("### 🎮 Camera Controls")
265
+ col1, col2, col3 = st.columns(3)
266
+ with col1:
267
+ st.button("⬅️", on_click=lambda: st.session_state.update({'camera_move':'left'}))
268
+ st.button("🔄↺", on_click=lambda: st.session_state.update({'camera_move':'rotateLeft'}))
269
+ st.button("🔝", on_click=lambda: st.session_state.update({'camera_move':'reset'}))
270
+ with col2:
271
+ st.button("⬆️", on_click=lambda: st.session_state.update({'camera_move':'up'}))
272
+ st.button("👀", on_click=lambda: st.session_state.update({'camera_move':'ground'}))
273
+ st.button("🔫", on_click=lambda: st.session_state.update({'camera_move':'fire'}))
274
+ with col3:
275
+ st.button("➡️", on_click=lambda: st.session_state.update({'camera_move':'right'}))
276
+ st.button("⬇️", on_click=lambda: st.session_state.update({'camera_move':'down'}))
277
+ st.button("⏩", on_click=lambda: st.session_state.update({'camera_move':'forward'}))
278
+
279
  st.markdown("### 🗺️ Grid Size")
280
+ grid_width = st.slider("Grid Width", 1, 8, 8)
281
+ grid_height = st.slider("Grid Height",1, 5, 5)
282
+
283
+ st.markdown("### 📁 Directory")
284
+ directory = st.text_input("Enter path:", ".", key="directory_input")
285
 
286
  if not os.path.isdir(directory):
287
+ st.sidebar.error("Invalid directory path")
288
  return
289
 
290
+ file_types = ['obj','glb','gltf','webp','png','mp4']
291
+ if uploaded_files:
292
+ for up in uploaded_files:
293
+ ext = Path(up.name).suffix.lower()[1:]
294
+ if ext in file_types:
295
+ with open(os.path.join(directory, up.name),"wb") as f:
296
+ shutil.copyfileobj(up, f)
297
+ st.sidebar.success(f"Uploaded: {up.name}")
298
+ else:
299
+ st.sidebar.warning(f"Skipped unsupported: {up.name}")
300
+
301
+ files = [f for f in os.listdir(directory) if f.split('.')[-1] in file_types]
302
 
303
+ # Build A-Frame scene shell
304
+ aframe_scene = f"""
305
  <a-scene embedded style="height:600px; width:100%;">
306
+ <a-entity id="rig" position="0 {max(grid_width,grid_height)} 0" rotation="-90 0 0">
307
+ <a-camera fov="60" look-controls cursor="rayOrigin: mouse" raycaster="objects:.raycastable"></a-camera>
 
 
308
  </a-entity>
309
  <a-sky color="#87CEEB"></a-sky>
310
  <a-entity moving-light="color:#FFD700; speed:0.07 0.05 0.06; bounds:4 3 4" position="2 2 -2"></a-entity>
 
313
  <a-text id="score" value="Score: 0" position="-1.5 1 -2" scale="0.5 0.5 0.5" color="white"></a-text>
314
  """
315
 
316
+ assets, entities = generate_tilemap(files, directory, grid_width, grid_height)
317
+ aframe_scene += assets + entities + "</a-scene>"
318
 
319
+ # Apply camera_move if any
320
+ cam = st.session_state.get('camera_move')
321
+ if cam:
322
+ if cam == 'fire':
323
+ aframe_scene += "<script>fireRaycast();</script>"
324
+ else:
325
+ aframe_scene += f"<script>moveCamera('{cam}');</script>"
326
  st.session_state.pop('camera_move')
327
 
328
+ # Loader for OBJ/glTF
 
329
  loader = '<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/aframe-extras.loaders.min.js"></script>'
330
+
331
  st.components.v1.html(
332
+ load_aframe_and_extras() + loader + aframe_scene,
333
+ height=630
334
  )
335
 
336
  if __name__ == "__main__":