awacke1 commited on
Commit
5b479a3
·
verified ·
1 Parent(s): cc49b29

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +277 -440
app.py CHANGED
@@ -1,441 +1,278 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Three.js Infinite World</title>
5
- <style>
6
- body { margin: 0; overflow: hidden; }
7
- canvas { display: block; }
8
- </style>
9
- <!-- New: Polling function for game state -->
10
- <script>
11
- // Poll the shared game state every 5 seconds (for demonstration)
12
- function pollGameState() {
13
- console.log("Polling updated game state:", window.GAME_STATE);
14
- // Here you could update the scene based on the new state.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  }
16
- setInterval(pollGameState, 5000);
17
- </script>
18
- </head>
19
- <body>
20
- <script type="importmap">
21
- {
22
- "imports": {
23
- "three": "https://unpkg.com/[email protected]/build/three.module.js",
24
- "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
25
- }
26
- }
27
- </script>
28
-
29
- <script type="module">
30
- import * as THREE from 'three';
31
-
32
- let scene, camera, renderer, playerMesh;
33
- let raycaster, mouse;
34
- const keysPressed = {};
35
- const playerSpeed = 0.15;
36
- let newlyPlacedObjects = []; // Track objects added THIS session for saving
37
- const placeholderPlots = new Set();
38
- const groundMeshes = {}; // Store ground mesh references
39
-
40
- // --- Session Storage Key ---
41
- const SESSION_STORAGE_KEY = 'unsavedInfiniteWorldState';
42
-
43
- // --- Injected State from Streamlit ---
44
- const allInitialObjects = window.ALL_INITIAL_OBJECTS || [];
45
- const plotsMetadata = window.PLOTS_METADATA || [];
46
- const selectedObjectType = window.SELECTED_OBJECT_TYPE || "None";
47
- const plotWidth = window.PLOT_WIDTH || 50.0;
48
- const plotDepth = window.PLOT_DEPTH || 50.0;
49
-
50
- const groundMaterial = new THREE.MeshStandardMaterial({
51
- color: 0x55aa55, roughness: 0.9, metalness: 0.1, side: THREE.DoubleSide
52
- });
53
- const placeholderGroundMaterial = new THREE.MeshStandardMaterial({
54
- color: 0x448844, roughness: 0.95, metalness: 0.1, side: THREE.DoubleSide
55
- });
56
-
57
- function init() {
58
- scene = new THREE.Scene();
59
- scene.background = new THREE.Color(0xabcdef);
60
- const aspect = window.innerWidth / window.innerHeight;
61
- camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 4000);
62
- camera.position.set(0, 15, 20);
63
- camera.lookAt(0, 0, 0);
64
- scene.add(camera);
65
-
66
- setupLighting();
67
- setupInitialGround();
68
- setupPlayer();
69
-
70
- raycaster = new THREE.Raycaster();
71
- mouse = new THREE.Vector2();
72
-
73
- renderer = new THREE.WebGLRenderer({ antialias: true });
74
- renderer.setSize(window.innerWidth, window.innerHeight);
75
- renderer.shadowMap.enabled = true;
76
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
77
- document.body.appendChild(renderer.domElement);
78
-
79
- loadInitialObjects();
80
- restoreUnsavedState();
81
-
82
- // Event Listeners
83
- document.addEventListener('mousemove', onMouseMove, false);
84
- document.addEventListener('click', onDocumentClick, false);
85
- window.addEventListener('resize', onWindowResize, false);
86
- document.addEventListener('keydown', onKeyDown);
87
- document.addEventListener('keyup', onKeyUp);
88
-
89
- // Define functions callable by Streamlit
90
- window.teleportPlayer = teleportPlayer;
91
- window.getSaveDataAndPosition = getSaveDataAndPosition;
92
- window.resetNewlyPlacedObjects = resetNewlyPlacedObjects;
93
-
94
- console.log("Three.js Initialized. World ready.");
95
- animate();
96
- }
97
-
98
- function setupLighting() {
99
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
100
- scene.add(ambientLight);
101
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
102
- directionalLight.position.set(50, 150, 100);
103
- directionalLight.castShadow = true;
104
- directionalLight.shadow.mapSize.width = 4096;
105
- directionalLight.shadow.mapSize.height = 4096;
106
- directionalLight.shadow.camera.near = 0.5;
107
- directionalLight.shadow.camera.far = 500;
108
- directionalLight.shadow.camera.left = -150;
109
- directionalLight.shadow.camera.right = 150;
110
- directionalLight.shadow.camera.top = 150;
111
- directionalLight.shadow.camera.bottom = -150;
112
- directionalLight.shadow.bias = -0.001;
113
- scene.add(directionalLight);
114
- }
115
-
116
- function setupInitialGround() {
117
- console.log(`Setting up initial ground for ${plotsMetadata.length} plots.`);
118
- plotsMetadata.forEach(plot => {
119
- createGroundPlane(plot.grid_x, plot.grid_z, false);
120
- });
121
- if (plotsMetadata.length === 0) {
122
- createGroundPlane(0, 0, false);
123
- }
124
- }
125
-
126
- function createGroundPlane(gridX, gridZ, isPlaceholder) {
127
- const gridKey = `${gridX}_${gridZ}`;
128
- if (groundMeshes[gridKey]) return;
129
- console.log(`Creating ${isPlaceholder ? 'placeholder' : 'initial'} ground at ${gridX}, ${gridZ}`);
130
- const groundGeometry = new THREE.PlaneGeometry(plotWidth, plotDepth);
131
- const material = isPlaceholder ? placeholderGroundMaterial : groundMaterial;
132
- const groundMesh = new THREE.Mesh(groundGeometry, material);
133
- groundMesh.rotation.x = -Math.PI / 2;
134
- groundMesh.position.y = -0.05;
135
- groundMesh.position.x = gridX * plotWidth + plotWidth / 2.0;
136
- groundMesh.position.z = gridZ * plotDepth + plotDepth / 2.0;
137
- groundMesh.receiveShadow = true;
138
- groundMesh.userData.gridKey = gridKey;
139
- scene.add(groundMesh);
140
- groundMeshes[gridKey] = groundMesh;
141
- if (isPlaceholder) placeholderPlots.add(gridKey);
142
- }
143
-
144
- function setupPlayer() {
145
- const playerGeo = new THREE.CapsuleGeometry(0.4, 0.8, 4, 8);
146
- const playerMat = new THREE.MeshStandardMaterial({ color: 0x0000ff, roughness: 0.6 });
147
- playerMesh = new THREE.Mesh(playerGeo, playerMat);
148
- playerMesh.position.set(plotWidth / 2, 0.4 + 0.8/2, plotDepth/2);
149
- playerMesh.castShadow = true;
150
- playerMesh.receiveShadow = true;
151
- scene.add(playerMesh);
152
- }
153
-
154
- function loadInitialObjects() {
155
- console.log(`Loading ${allInitialObjects.length} initial objects from Python.`);
156
- allInitialObjects.forEach(objData => { createAndPlaceObject(objData, false); });
157
- console.log("Finished loading initial objects.");
158
- }
159
-
160
- function createAndPlaceObject(objData, isNewObject) {
161
- let loadedObject = null;
162
- switch (objData.type) {
163
- case "Simple House": loadedObject = createSimpleHouse(); break;
164
- case "Tree": loadedObject = createTree(); break;
165
- case "Rock": loadedObject = createRock(); break;
166
- case "Fence Post": loadedObject = createFencePost(); break;
167
- default: console.warn("Unknown object type in data:", objData.type); break;
168
- }
169
- if (loadedObject) {
170
- if (objData.position && objData.position.x !== undefined) {
171
- loadedObject.position.set(objData.position.x, objData.position.y, objData.position.z);
172
- } else if (objData.pos_x !== undefined) {
173
- loadedObject.position.set(objData.pos_x, objData.pos_y, objData.pos_z);
174
- }
175
- if (objData.rotation) {
176
- loadedObject.rotation.set(objData.rotation._x, objData.rotation._y, objData.rotation._z, objData.rotation._order || 'XYZ');
177
- } else if (objData.rot_x !== undefined) {
178
- loadedObject.rotation.set(objData.rot_x, objData.rot_y, objData.rot_z, objData.rot_order || 'XYZ');
179
- }
180
- loadedObject.userData.obj_id = objData.obj_id || loadedObject.userData.obj_id;
181
- scene.add(loadedObject);
182
- if (isNewObject) newlyPlacedObjects.push(loadedObject);
183
- return loadedObject;
184
- }
185
- return null;
186
- }
187
-
188
- function saveUnsavedState() {
189
- try {
190
- const stateToSave = newlyPlacedObjects.map(obj => ({
191
- obj_id: obj.userData.obj_id,
192
- type: obj.userData.type,
193
- position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
194
- rotation: { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order }
195
- }));
196
- sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(stateToSave));
197
- console.log(`Saved ${stateToSave.length} unsaved objects to sessionStorage.`);
198
- } catch (e) {
199
- console.error("Error saving state to sessionStorage:", e);
200
- }
201
- }
202
-
203
- function restoreUnsavedState() {
204
- try {
205
- const savedState = sessionStorage.getItem(SESSION_STORAGE_KEY);
206
- if (savedState) {
207
- console.log("Found unsaved state in sessionStorage. Restoring...");
208
- const objectsToRestore = JSON.parse(savedState);
209
- if (Array.isArray(objectsToRestore)) {
210
- newlyPlacedObjects = [];
211
- let count = 0;
212
- objectsToRestore.forEach(objData => {
213
- if(createAndPlaceObject(objData, true)) { count++; }
214
- });
215
- console.log(`Restored ${count} objects.`);
216
- }
217
- } else {
218
- console.log("No unsaved state found in sessionStorage.");
219
- }
220
- } catch (e) {
221
- console.error("Error restoring state from sessionStorage:", e);
222
- sessionStorage.removeItem(SESSION_STORAGE_KEY);
223
- }
224
- }
225
-
226
- function clearUnsavedState() {
227
- sessionStorage.removeItem(SESSION_STORAGE_KEY);
228
- newlyPlacedObjects = [];
229
- console.log("Cleared unsaved state from memory and sessionStorage.");
230
- }
231
-
232
- function createObjectBase(type) {
233
- return { userData: { type: type, obj_id: THREE.MathUtils.generateUUID() } };
234
- }
235
-
236
- function createSimpleHouse() {
237
- const base = createObjectBase("Simple House");
238
- const group = new THREE.Group();
239
- Object.assign(group, base);
240
- const mat1 = new THREE.MeshStandardMaterial({color:0xffccaa, roughness:0.8});
241
- const mat2 = new THREE.MeshStandardMaterial({color:0xaa5533, roughness:0.7});
242
- const m1 = new THREE.Mesh(new THREE.BoxGeometry(2,1.5,2.5), mat1);
243
- m1.position.y = 1.5/2;
244
- m1.castShadow = true;
245
- m1.receiveShadow = true;
246
- group.add(m1);
247
- const m2 = new THREE.Mesh(new THREE.ConeGeometry(1.8,1,4), mat2);
248
- m2.position.y = 1.5+1/2;
249
- m2.rotation.y = Math.PI/4;
250
- m2.castShadow = true;
251
- m2.receiveShadow = true;
252
- group.add(m2);
253
- return group;
254
- }
255
-
256
- function createTree() {
257
- const base = createObjectBase("Tree");
258
- const group = new THREE.Group();
259
- Object.assign(group, base);
260
- const mat1 = new THREE.MeshStandardMaterial({color:0x8B4513, roughness:0.9});
261
- const mat2 = new THREE.MeshStandardMaterial({color:0x228B22, roughness:0.8});
262
- const m1 = new THREE.Mesh(new THREE.CylinderGeometry(0.3,0.4,2,8), mat1);
263
- m1.position.y = 1;
264
- m1.castShadow = true;
265
- m1.receiveShadow = true;
266
- group.add(m1);
267
- const m2 = new THREE.Mesh(new THREE.IcosahedronGeometry(1.2,0), mat2);
268
- m2.position.y = 2.8;
269
- m2.castShadow = true;
270
- m2.receiveShadow = true;
271
- group.add(m2);
272
- return group;
273
- }
274
-
275
- function createRock() {
276
- const base = createObjectBase("Rock");
277
- const mat = new THREE.MeshStandardMaterial({color:0xaaaaaa, roughness:0.8, metalness:0.1});
278
- const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.7,0), mat);
279
- Object.assign(rock, base);
280
- rock.position.y = 0.35;
281
- rock.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, 0);
282
- rock.castShadow = true;
283
- rock.receiveShadow = true;
284
- return rock;
285
- }
286
-
287
- function createFencePost() {
288
- const base = createObjectBase("Fence Post");
289
- const mat = new THREE.MeshStandardMaterial({color:0xdeb887, roughness:0.9});
290
- const post = new THREE.Mesh(new THREE.BoxGeometry(0.2,1.5,0.2), mat);
291
- Object.assign(post, base);
292
- post.position.y = 0.75;
293
- post.castShadow = true;
294
- post.receiveShadow = true;
295
- return post;
296
- }
297
-
298
- function onMouseMove(event) {
299
- mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
300
- mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
301
- }
302
-
303
- function onDocumentClick(event) {
304
- if (selectedObjectType === "None") return;
305
- const groundCandidates = Object.values(groundMeshes);
306
- if (groundCandidates.length === 0) return;
307
- raycaster.setFromCamera(mouse, camera);
308
- const intersects = raycaster.intersectObjects(groundCandidates);
309
- if (intersects.length > 0) {
310
- const intersectPoint = intersects[0].point;
311
- let newObjectToPlace = null;
312
- switch (selectedObjectType) {
313
- case "Simple House": newObjectToPlace = createSimpleHouse(); break;
314
- case "Tree": newObjectToPlace = createTree(); break;
315
- case "Rock": newObjectToPlace = createRock(); break;
316
- case "Fence Post": newObjectToPlace = createFencePost(); break;
317
- default: return;
318
- }
319
- if (newObjectToPlace) {
320
- newObjectToPlace.position.copy(intersectPoint);
321
- newObjectToPlace.position.y += 0.01;
322
- scene.add(newObjectToPlace);
323
- newlyPlacedObjects.push(newObjectToPlace);
324
- saveUnsavedState();
325
- console.log(`Placed new ${selectedObjectType}. Total unsaved: ${newlyPlacedObjects.length}`);
326
- }
327
- }
328
- }
329
-
330
- function onKeyDown(event) { keysPressed[event.code] = true; }
331
- function onKeyUp(event) { keysPressed[event.code] = false; }
332
-
333
- function teleportPlayer(targetX, targetZ) {
334
- console.log(`JS teleportPlayer called with targetX: ${targetX}, targetZ: ${targetZ}`);
335
- if (playerMesh) {
336
- playerMesh.position.x = targetX;
337
- playerMesh.position.z = targetZ;
338
- const offset = new THREE.Vector3(0, 15, 20);
339
- const targetPosition = playerMesh.position.clone().add(offset);
340
- camera.position.copy(targetPosition);
341
- camera.lookAt(playerMesh.position);
342
- console.log("Player teleported to:", playerMesh.position);
343
- } else {
344
- console.error("Player mesh not found for teleport.");
345
- }
346
- }
347
-
348
- function getSaveDataAndPosition() {
349
- console.log(`JS getSaveDataAndPosition called. Found ${newlyPlacedObjects.length} new objects.`);
350
- const objectsToSave = newlyPlacedObjects.map(obj => {
351
- if (!obj.userData || !obj.userData.type) { return null; }
352
- const rotation = { _x: obj.rotation.x, _y: obj.rotation.y, _z: obj.rotation.z, _order: obj.rotation.order };
353
- return {
354
- obj_id: obj.userData.obj_id, type: obj.userData.type,
355
- position: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
356
- rotation: rotation
357
- };
358
- }).filter(obj => obj !== null);
359
- const playerPos = playerMesh ? { x: playerMesh.position.x, y: playerMesh.position.y, z: playerMesh.position.z } : {x:0, y:0, z:0};
360
- const payload = {
361
- playerPosition: playerPos,
362
- objectsToSave: objectsToSave
363
- };
364
- console.log("Prepared payload for saving:", payload);
365
- return JSON.stringify(payload);
366
- }
367
-
368
- function resetNewlyPlacedObjects() {
369
- console.log(`JS resetNewlyPlacedObjects called.`);
370
- clearUnsavedState();
371
- }
372
-
373
- function updatePlayerMovement() {
374
- if (!playerMesh) return;
375
- const moveDirection = new THREE.Vector3(0, 0, 0);
376
- if (keysPressed['KeyW'] || keysPressed['ArrowUp']) moveDirection.z -= 1;
377
- if (keysPressed['KeyS'] || keysPressed['ArrowDown']) moveDirection.z += 1;
378
- if (keysPressed['KeyA'] || keysPressed['ArrowLeft']) moveDirection.x -= 1;
379
- if (keysPressed['KeyD'] || keysPressed['ArrowRight']) moveDirection.x += 1;
380
- if (moveDirection.lengthSq() > 0) {
381
- moveDirection.normalize().multiplyScalar(playerSpeed);
382
- const forward = new THREE.Vector3();
383
- camera.getWorldDirection(forward);
384
- forward.y = 0;
385
- forward.normalize();
386
- const right = new THREE.Vector3().crossVectors(camera.up, forward).normalize();
387
- const worldMove = new THREE.Vector3();
388
- worldMove.add(forward.multiplyScalar(-moveDirection.z));
389
- worldMove.add(right.multiplyScalar(-moveDirection.x));
390
- worldMove.normalize().multiplyScalar(playerSpeed);
391
- playerMesh.position.add(worldMove);
392
- playerMesh.position.y = Math.max(playerMesh.position.y, 0.4 + 0.8/2);
393
- checkAndExpandGround();
394
- }
395
- }
396
-
397
- function checkAndExpandGround() {
398
- if (!playerMesh) return;
399
- const currentGridX = Math.floor(playerMesh.position.x / plotWidth);
400
- const currentGridZ = Math.floor(playerMesh.position.z / plotDepth);
401
- for (let dx = -1; dx <= 1; dx++) {
402
- for (let dz = -1; dz <= 1; dz++) {
403
- if (dx === 0 && dz === 0) continue;
404
- const checkX = currentGridX + dx;
405
- const checkZ = currentGridZ + dz;
406
- const gridKey = `${checkX}_${checkZ}`;
407
- if (!groundMeshes[gridKey]) {
408
- const isSavedPlot = plotsMetadata.some(plot => plot.grid_x === checkX && plot.grid_z === checkZ);
409
- if (!isSavedPlot) {
410
- createGroundPlane(checkX, checkZ, true);
411
- }
412
- }
413
- }
414
- }
415
- }
416
-
417
- function updateCamera() {
418
- if (!playerMesh) return;
419
- const offset = new THREE.Vector3(0, 15, 20);
420
- const targetPosition = playerMesh.position.clone().add(offset);
421
- camera.position.lerp(targetPosition, 0.08);
422
- camera.lookAt(playerMesh.position);
423
- }
424
-
425
- function onWindowResize() {
426
- camera.aspect = window.innerWidth / window.innerHeight;
427
- camera.updateProjectionMatrix();
428
- renderer.setSize(window.innerWidth, window.innerHeight);
429
- }
430
-
431
- function animate() {
432
- requestAnimationFrame(animate);
433
- updatePlayerMovement();
434
- updateCamera();
435
- renderer.render(scene, camera);
436
- }
437
-
438
- init();
439
- </script>
440
- </body>
441
- </html>
 
1
+ # app.py
2
+ import streamlit as st
3
+ import streamlit.components.v1 as components
4
+ import os
5
+ import json
6
+ import pandas as pd
7
+ import uuid
8
+ import math
9
+ import time
10
+
11
+ # Import our GameState class
12
+ from gamestate import GameState
13
+
14
+ # --- Constants ---
15
+ SAVE_DIR = "saved_worlds"
16
+ PLOT_WIDTH = 50.0 # Width of each plot in 3D space
17
+ PLOT_DEPTH = 50.0 # Depth of each plot
18
+ CSV_COLUMNS = ['obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'rot_z', 'rot_order']
19
+
20
+ # --- Ensure Save Directory Exists ---
21
+ os.makedirs(SAVE_DIR, exist_ok=True)
22
+
23
+ @st.cache_data(ttl=3600)
24
+ def load_plot_metadata():
25
+ """Scans SAVE_DIR for plot files and returns metadata."""
26
+ plots = []
27
+ try:
28
+ plot_files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")]
29
+ except FileNotFoundError:
30
+ st.error(f"Save directory '{SAVE_DIR}' not found.")
31
+ return []
32
+ except Exception as e:
33
+ st.error(f"Error listing save directory '{SAVE_DIR}': {e}")
34
+ return []
35
+
36
+ parsed_plots = []
37
+ for filename in plot_files:
38
+ try:
39
+ parts = filename[:-4].split('_') # Remove .csv
40
+ grid_x = int(parts[1][1:]) # After 'X'
41
+ grid_z = int(parts[2][1:]) # After 'Z'
42
+ plot_name = " ".join(parts[3:]) if len(parts) > 3 else f"Plot ({grid_x},{grid_z})"
43
+ parsed_plots.append({
44
+ 'id': filename[:-4],
45
+ 'filename': filename,
46
+ 'grid_x': grid_x,
47
+ 'grid_z': grid_z,
48
+ 'name': plot_name,
49
+ 'x_offset': grid_x * PLOT_WIDTH,
50
+ 'z_offset': grid_z * PLOT_DEPTH
51
+ })
52
+ except (IndexError, ValueError):
53
+ st.warning(f"Could not parse grid coordinates from filename: {filename}. Skipping.")
54
+ continue
55
+
56
+ parsed_plots.sort(key=lambda p: (p['grid_x'], p['grid_z']))
57
+ return parsed_plots
58
+
59
+ def load_plot_objects(filename, x_offset, z_offset):
60
+ """Loads objects from a CSV file and applies world offsets."""
61
+ file_path = os.path.join(SAVE_DIR, filename)
62
+ objects = []
63
+ try:
64
+ df = pd.read_csv(file_path)
65
+ if not all(col in df.columns for col in ['type', 'pos_x', 'pos_y', 'pos_z']):
66
+ st.warning(f"CSV '{filename}' missing essential columns. Skipping.")
67
+ return []
68
+ df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in range(len(df))]))
69
+ for col, default in [('rot_x', 0.0), ('rot_y', 0.0), ('rot_z', 0.0), ('rot_order', 'XYZ')]:
70
+ if col not in df.columns:
71
+ df[col] = default
72
+
73
+ for _, row in df.iterrows():
74
+ obj_data = row.to_dict()
75
+ obj_data['pos_x'] += x_offset
76
+ obj_data['pos_z'] += z_offset
77
+ objects.append(obj_data)
78
+ return objects
79
+ except FileNotFoundError:
80
+ st.error(f"File not found during object load: {filename}")
81
+ return []
82
+ except pd.errors.EmptyDataError:
83
+ return []
84
+ except Exception as e:
85
+ st.error(f"Error loading objects from {filename}: {e}")
86
+ return []
87
+
88
+ def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
89
+ """Saves object data list to a CSV file, making positions relative to the plot origin."""
90
+ file_path = os.path.join(SAVE_DIR, filename)
91
+ relative_objects = []
92
+ if not isinstance(objects_data_list, list):
93
+ st.error("Invalid data format received for saving (expected a list).")
94
+ return False
95
+
96
+ for obj in objects_data_list:
97
+ pos = obj.get('position', {})
98
+ rot = obj.get('rotation', {})
99
+ obj_type = obj.get('type', 'Unknown')
100
+ obj_id = obj.get('obj_id', str(uuid.uuid4()))
101
+
102
+ if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
103
+ print(f"Skipping malformed object during save prep: {obj}")
104
+ continue
105
+
106
+ relative_obj = {
107
+ 'obj_id': obj_id, 'type': obj_type,
108
+ 'pos_x': pos.get('x', 0.0) - plot_x_offset,
109
+ 'pos_y': pos.get('y', 0.0),
110
+ 'pos_z': pos.get('z', 0.0) - plot_z_offset,
111
+ 'rot_x': rot.get('_x', 0.0), 'rot_y': rot.get('_y', 0.0),
112
+ 'rot_z': rot.get('_z', 0.0), 'rot_order': rot.get('_order', 'XYZ')
113
  }
114
+ relative_objects.append(relative_obj)
115
+
116
+ try:
117
+ df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
118
+ df.to_csv(file_path, index=False)
119
+ st.success(f"Saved {len(relative_objects)} objects to {filename}")
120
+ return True
121
+ except Exception as e:
122
+ st.error(f"Failed to save plot data to {filename}: {e}")
123
+ return False
124
+
125
+ # --- Initialize GameState Singleton ---
126
+ @st.cache_resource
127
+ def get_game_state():
128
+ # This instance is shared across all sessions and reruns.
129
+ return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv")
130
+
131
+ game_state = get_game_state()
132
+
133
+ # --- Page Config ---
134
+ st.set_page_config(page_title="Infinite World Builder", layout="wide")
135
+
136
+ # --- Session State Initialization ---
137
+ if 'selected_object' not in st.session_state:
138
+ st.session_state.selected_object = 'None'
139
+ if 'new_plot_name' not in st.session_state:
140
+ st.session_state.new_plot_name = ""
141
+ if 'js_save_data_result' not in st.session_state:
142
+ st.session_state.js_save_data_result = None
143
+
144
+ plots_metadata = load_plot_metadata()
145
+ all_initial_objects = []
146
+ for plot in plots_metadata:
147
+ all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
148
+
149
+ # --- Sidebar ---
150
+ with st.sidebar:
151
+ st.title("🏗️ World Controls")
152
+ st.header("Navigation (Plots)")
153
+ st.caption("Click to teleport player to a plot.")
154
+ max_cols = 2
155
+ cols = st.columns(max_cols)
156
+ col_idx = 0
157
+ sorted_plots_for_nav = sorted(plots_metadata, key=lambda p: (p['grid_x'], p['grid_z']))
158
+ for plot in sorted_plots_for_nav:
159
+ button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
160
+ if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
161
+ target_x = plot['x_offset']
162
+ target_z = plot['z_offset']
163
+ try:
164
+ js_code = f"teleportPlayer({target_x + PLOT_WIDTH/2}, {target_z + PLOT_DEPTH/2});"
165
+ from streamlit_js_eval import streamlit_js_eval
166
+ streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
167
+ except Exception as e:
168
+ st.error(f"Failed to send teleport command: {e}")
169
+ col_idx = (col_idx + 1) % max_cols
170
+
171
+ st.markdown("---")
172
+ st.header("Place Objects")
173
+ object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
174
+ current_object_index = object_types.index(st.session_state.selected_object) if st.session_state.selected_object in object_types else 0
175
+ selected_object_type_widget = st.selectbox("Select Object:", options=object_types, index=current_object_index, key="selected_object_widget")
176
+ if selected_object_type_widget != st.session_state.selected_object:
177
+ st.session_state.selected_object = selected_object_type_widget
178
+
179
+ st.markdown("---")
180
+ st.header("Save Work")
181
+ st.caption("Saves newly placed objects to the current plot. A new plot file is created for new areas.")
182
+ if st.button("💾 Save Current Work", key="save_button"):
183
+ from streamlit_js_eval import streamlit_js_eval
184
+ js_get_data_code = "getSaveDataAndPosition();"
185
+ streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
186
+ st.rerun()
187
+
188
+ # --- Process Save Data from JS ---
189
+ save_data_from_js = st.session_state.get("js_save_processor", None)
190
+ if save_data_from_js is not None:
191
+ st.info("Received save data from client...")
192
+ save_processed_successfully = False
193
+ try:
194
+ payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
195
+ if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
196
+ player_pos = payload['playerPosition']
197
+ objects_to_save = payload['objectsToSave']
198
+ if isinstance(objects_to_save, list):
199
+ target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
200
+ target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
201
+ target_filename = f"plot_X{target_grid_x}_Z{target_grid_z}.csv"
202
+ target_plot_x_offset = target_grid_x * PLOT_WIDTH
203
+ target_plot_z_offset = target_grid_z * PLOT_DEPTH
204
+ st.write(f"Attempting to save plot: {target_filename} (Player at: x={player_pos.get('x', 0):.1f}, z={player_pos.get('z', 0):.1f})")
205
+ is_new_plot_file = not os.path.exists(os.path.join(SAVE_DIR, target_filename))
206
+ save_ok = save_plot_data(target_filename, objects_to_save, target_plot_x_offset, target_plot_z_offset)
207
+ if save_ok:
208
+ load_plot_metadata.clear() # Clear cache so metadata reloads
209
+ try:
210
+ from streamlit_js_eval import streamlit_js_eval
211
+ streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js_state")
212
+ except Exception as js_e:
213
+ st.warning(f"Could not reset JS state after save: {js_e}")
214
+ if is_new_plot_file:
215
+ st.success(f"New plot created and saved: {target_filename}")
216
+ else:
217
+ st.success(f"Updated existing plot: {target_filename}")
218
+ # Update shared game state with new objects from this session
219
+ game_state.update_state(objects_to_save)
220
+ save_processed_successfully = True
221
+ else:
222
+ st.error(f"Failed to save plot data to file: {target_filename}")
223
+ else:
224
+ st.error("Invalid 'objectsToSave' format received (expected list).")
225
+ else:
226
+ st.error("Invalid save payload structure received from client.")
227
+ except json.JSONDecodeError:
228
+ st.error("Failed to decode save data from client.")
229
+ except Exception as e:
230
+ st.error(f"Error processing save: {e}")
231
+ st.session_state.js_save_processor = None
232
+ if save_processed_successfully:
233
+ st.rerun()
234
+
235
+ # --- Main Area ---
236
+ st.header("Infinite Shared 3D World")
237
+ st.caption("Move to empty areas to expand the world. Use the sidebar 'Save' to store your work.")
238
+
239
+ # Inject state into JS—including the shared GAME_STATE from our GameState singleton.
240
+ injected_state = {
241
+ "ALL_INITIAL_OBJECTS": all_initial_objects,
242
+ "PLOTS_METADATA": plots_metadata,
243
+ "SELECTED_OBJECT_TYPE": st.session_state.selected_object,
244
+ "PLOT_WIDTH": PLOT_WIDTH,
245
+ "PLOT_DEPTH": PLOT_DEPTH,
246
+ "GAME_STATE": game_state.get_state()
247
+ }
248
+
249
+ html_file_path = 'index.html'
250
+ html_content_with_state = None
251
+ try:
252
+ with open(html_file_path, 'r', encoding='utf-8') as f:
253
+ html_template = f.read()
254
+
255
+ js_injection_script = f"""
256
+ <script>
257
+ window.ALL_INITIAL_OBJECTS = {json.dumps(injected_state["ALL_INITIAL_OBJECTS"])};
258
+ window.PLOTS_METADATA = {json.dumps(injected_state["PLOTS_METADATA"])};
259
+ window.SELECTED_OBJECT_TYPE = {json.dumps(injected_state["SELECTED_OBJECT_TYPE"])};
260
+ window.PLOT_WIDTH = {json.dumps(injected_state["PLOT_WIDTH"])};
261
+ window.PLOT_DEPTH = {json.dumps(injected_state["PLOT_DEPTH"])};
262
+ window.GAME_STATE = {json.dumps(injected_state["GAME_STATE"])};
263
+ console.log("Streamlit State Injected:", {{
264
+ selectedObject: window.SELECTED_OBJECT_TYPE,
265
+ initialObjectsCount: window.ALL_INITIAL_OBJECTS ? window.ALL_INITIAL_OBJECTS.length : 0,
266
+ plotCount: window.PLOTS_METADATA ? window.PLOTS_METADATA.length : 0,
267
+ gameStateObjects: window.GAME_STATE ? window.GAME_STATE.length : 0
268
+ }});
269
+ </script>
270
+ """
271
+ html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
272
+ components.html(html_content_with_state, height=750, scrolling=False)
273
+ except FileNotFoundError:
274
+ st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
275
+ st.warning(f"Make sure `{html_file_path}` is in the same directory as `app.py` and `{SAVE_DIR}` exists.")
276
+ except Exception as e:
277
+ st.error(f"An critical error occurred during HTML preparation or component rendering: {e}")
278
+ st.exception(e)