awacke1 commited on
Commit
14b289d
·
verified ·
1 Parent(s): d93d024

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +73 -93
app.py CHANGED
@@ -6,13 +6,12 @@ import json
6
  import pandas as pd
7
  import uuid
8
  import math # For floor function
9
- # from PIL import Image, ImageDraw # No longer needed for minimap
10
  from streamlit_js_eval import streamlit_js_eval # For JS communication
11
 
12
  # --- Constants ---
13
  SAVE_DIR = "saved_worlds"
14
  PLOT_WIDTH = 50.0 # Width of each plot in 3D space
15
- PLOT_DEPTH = 50.0 # Depth of each plot (can be same as width)
16
  CSV_COLUMNS = ['obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'rot_z', 'rot_order']
17
 
18
  # --- Ensure Save Directory Exists ---
@@ -20,83 +19,77 @@ os.makedirs(SAVE_DIR, exist_ok=True)
20
 
21
  # --- Helper Functions ---
22
 
23
- @st.cache_data(ttl=3600) # Cache plot list
24
- def load_plot_metadata():
 
25
  """Scans save dir for plot_X*_Z*.csv, sorts, calculates metadata."""
 
 
 
 
26
  plots = []
27
  plot_files = []
28
- st.write(f"Scanning directory: {os.path.abspath(SAVE_DIR)}") # Debug print
29
  try:
30
  plot_files = sorted([f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")])
31
- st.write(f"Found files: {plot_files}") # Debug print
32
  except FileNotFoundError:
33
  st.error(f"Save directory '{SAVE_DIR}' not found.")
34
- return []
35
  except Exception as e:
36
  st.error(f"Error listing save directory '{SAVE_DIR}': {e}")
37
  return []
38
 
39
- # Parse filenames to get grid coordinates
40
  parsed_plots = []
41
  for filename in plot_files:
42
  try:
43
- parts = filename[:-4].split('_') # Remove .csv
44
- grid_x = int(parts[1][1:]) # Extract number after X
45
- grid_z = int(parts[2][1:]) # Extract number after Z
46
- # Extract name if present (parts after Z coordinate)
47
  plot_name = " ".join(parts[3:]) if len(parts) > 3 else f"Plot ({grid_x},{grid_z})"
48
 
49
  parsed_plots.append({
50
- 'id': filename[:-4], # Use filename base as unique ID
51
  'filename': filename,
52
- 'grid_x': grid_x,
53
- 'grid_z': grid_z,
54
- 'name': plot_name,
55
  'x_offset': grid_x * PLOT_WIDTH,
56
- 'z_offset': grid_z * PLOT_DEPTH # Use PLOT_DEPTH for Z offset
57
  })
58
  except (IndexError, ValueError):
59
  st.warning(f"Could not parse grid coordinates from filename: {filename}. Skipping.")
60
  continue
61
 
62
- # Sort primarily by X, then by Z
63
  parsed_plots.sort(key=lambda p: (p['grid_x'], p['grid_z']))
64
- st.write(f"Loaded metadata for {len(parsed_plots)} plots.") # Debug print
65
  return parsed_plots
66
 
 
67
  def load_plot_objects(filename, x_offset, z_offset):
68
  """Loads objects from a CSV, applying the plot's world offsets."""
69
  file_path = os.path.join(SAVE_DIR, filename)
70
  objects = []
71
  try:
72
  df = pd.read_csv(file_path)
73
- # Check required columns
74
  if not all(col in df.columns for col in ['type', 'pos_x', 'pos_y', 'pos_z']):
75
- # st.warning(f"CSV '{filename}' missing essential columns. Skipping.") # Can be noisy
76
- return []
77
- # Add defaults for optional columns
78
  df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in range(len(df))]))
79
  for col, default in [('rot_x', 0.0), ('rot_y', 0.0), ('rot_z', 0.0), ('rot_order', 'XYZ')]:
80
  if col not in df.columns: df[col] = default
81
 
82
  for _, row in df.iterrows():
83
  obj_data = row.to_dict()
84
- # Apply world offset
85
  obj_data['pos_x'] += x_offset
86
- obj_data['pos_z'] += z_offset # Apply Z offset too
87
  objects.append(obj_data)
88
  return objects
89
- except FileNotFoundError:
90
- st.error(f"File not found during object load: {filename}")
91
- return []
92
- except pd.errors.EmptyDataError:
93
- return [] # Empty file is valid
94
  except Exception as e:
95
  st.error(f"Error loading objects from {filename}: {e}")
96
  return []
97
 
 
98
  def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
99
- """Saves object data list to a CSV, making positions relative to plot origin."""
100
  file_path = os.path.join(SAVE_DIR, filename)
101
  relative_objects = []
102
  if not isinstance(objects_data_list, list):
@@ -107,6 +100,7 @@ def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
107
  pos = obj.get('position', {})
108
  rot = obj.get('rotation', {})
109
  obj_type = obj.get('type', 'Unknown')
 
110
  obj_id = obj.get('obj_id', str(uuid.uuid4()))
111
 
112
  if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
@@ -124,66 +118,60 @@ def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
124
  relative_objects.append(relative_obj)
125
 
126
  try:
 
127
  df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
128
  df.to_csv(file_path, index=False)
129
- # Success message handled in the calling block
130
- # st.success(f"Saved {len(relative_objects)} objects to {filename}")
131
  return True
132
  except Exception as e:
133
  st.error(f"Failed to save plot data to {filename}: {e}")
134
  return False
135
 
136
  # --- Page Config ---
137
- st.set_page_config( page_title="Infinite World Builder", layout="wide")
138
 
139
  # --- Initialize Session State ---
140
  if 'selected_object' not in st.session_state: st.session_state.selected_object = 'None'
141
- # Removed 'new_plot_name' state as it wasn't used for saving anymore
142
  if 'js_save_data_result' not in st.session_state: st.session_state.js_save_data_result = None
 
 
143
 
144
- # --- Load Plot Metadata ---
145
- # Cached function returns list of plots
146
- # Note: next_plot_x_offset isn't returned/needed anymore
147
- plots_metadata = load_plot_metadata()
148
 
149
  # --- Load ALL Objects for Rendering ---
150
  all_initial_objects = []
151
- st.write(f"Loading objects for {len(plots_metadata)} plots...") # Debug
152
  for plot in plots_metadata:
153
- loaded_count = len(all_initial_objects)
154
  all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
155
- # st.write(f" Loaded {len(all_initial_objects) - loaded_count} objects from {plot['filename']}") # Verbose Debug
156
- st.write(f"Total objects loaded: {len(all_initial_objects)}") # Debug
157
 
158
  # --- Sidebar ---
159
  with st.sidebar:
160
  st.title("🏗️ World Controls")
161
 
162
- # *** NEW: Refresh Button ***
163
  if st.button("🔄 Refresh World View", key="refresh_button"):
164
- st.info("Clearing cache and reloading world data...")
165
- load_plot_metadata.clear() # Clear the cache for plot metadata
166
- # No need to clear all_initial_objects, it's rebuilt after rerun
167
- st.rerun() # Force the script to rerun from the top
168
 
169
  st.header("Navigation (Plots)")
170
  st.caption("Click to teleport player to a plot.")
171
- max_cols = 2 # Adjust columns for potentially more buttons
172
  cols = st.columns(max_cols)
173
  col_idx = 0
174
- # Sort buttons by grid coords for logical layout
175
  sorted_plots_for_nav = sorted(plots_metadata, key=lambda p: (p['grid_x'], p['grid_z']))
176
  for plot in sorted_plots_for_nav:
177
  button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
178
  if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
179
- # Teleport near center of plot
180
- target_x = plot['x_offset'] + PLOT_WIDTH / 2
181
- target_z = plot['z_offset'] + PLOT_DEPTH / 2
182
  try:
183
  js_code = f"teleportPlayer({target_x}, {target_z});"
184
  streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
185
- except Exception as e:
186
- st.error(f"Failed to send teleport command: {e}")
187
  col_idx = (col_idx + 1) % max_cols
188
 
189
  st.markdown("---")
@@ -197,34 +185,35 @@ with st.sidebar:
197
  )
198
  if selected_object_type_widget != st.session_state.selected_object:
199
  st.session_state.selected_object = selected_object_type_widget
200
- # Selecting causes rerun, state injection handles update in JS, sessionStorage preserves placed objects
201
 
202
  st.markdown("---")
203
 
204
  # --- Saving ---
205
  st.header("Save Work")
206
- st.caption("Saves newly placed objects to the plot the player is currently in. If it's a new area, a new plot file is created.")
207
- if st.button("💾 Save Current Work", key="save_button"):
208
- # Trigger JS to get data AND player position
209
- js_get_data_code = "getSaveDataAndPosition();" # JS function name
210
- streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor") # Store result
211
- st.rerun() # Rerun to process result
212
 
213
 
214
  # --- Process Save Data ---
215
  save_data_from_js = st.session_state.get("js_save_processor", None)
216
 
217
- if save_data_from_js is not None: # Process only if data is present
218
- st.info("Received save data from client...")
219
  save_processed_successfully = False
220
  try:
221
  payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
222
 
223
  if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
224
  player_pos = payload['playerPosition']
 
225
  objects_to_save = payload['objectsToSave']
226
 
227
- if isinstance(objects_to_save, list): # Allow saving empty list
228
  # Determine target plot based on player position
229
  target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
230
  target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
@@ -233,26 +222,23 @@ if save_data_from_js is not None: # Process only if data is present
233
  target_plot_x_offset = target_grid_x * PLOT_WIDTH
234
  target_plot_z_offset = target_grid_z * PLOT_DEPTH
235
 
236
- 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})")
237
  is_new_plot_file = not os.path.exists(os.path.join(SAVE_DIR, target_filename))
238
 
239
- # --- Save the data ---
240
  save_ok = save_plot_data(target_filename, objects_to_save, target_plot_x_offset, target_plot_z_offset)
241
 
242
  if save_ok:
243
- load_plot_metadata.clear() # Clear cache crucial for others/refresh
244
- try: # Tell JS to clear its unsaved state
245
- streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js_state")
246
- except Exception as js_e:
247
- st.warning(f"Could not reset JS state after save: {js_e}")
248
-
249
- if is_new_plot_file: st.success(f"New plot created and saved: {target_filename}")
250
  else: st.success(f"Updated existing plot: {target_filename}")
251
  save_processed_successfully = True
252
  else:
253
  st.error(f"Failed to save plot data to file: {target_filename}")
254
  else: st.error("Invalid 'objectsToSave' format (expected list).")
255
- else: st.error("Invalid save payload structure received.")
256
 
257
  except json.JSONDecodeError:
258
  st.error("Failed to decode save data from client.")
@@ -261,55 +247,49 @@ if save_data_from_js is not None: # Process only if data is present
261
  st.error(f"Error processing save: {e}")
262
  st.exception(e)
263
 
264
- # Clear the trigger data from session state IMPORTANT!
265
  st.session_state.js_save_processor = None
266
- # Rerun after processing save to reflect changes
267
  if save_processed_successfully:
 
268
  st.rerun()
269
 
270
 
271
  # --- Main Area ---
272
- st.header("Infinite Shared 3D World")
273
- st.caption(f"Location: {os.getcwd()}. Saving to sub-directory: '{SAVE_DIR}'. Plots loaded: {len(plots_metadata)}")
274
 
275
  # --- Load and Prepare HTML ---
276
  html_file_path = 'index.html'
277
- html_content_with_state = None # Initialize
278
 
279
  try:
280
  with open(html_file_path, 'r', encoding='utf-8') as f:
281
  html_template = f.read()
282
 
283
- # --- Inject Python state into JavaScript ---
284
- # Send plot metadata so JS knows where existing ground is
285
  js_injection_script = f"""
286
  <script>
 
287
  window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)};
288
- window.PLOTS_METADATA = {json.dumps(plots_metadata)};
289
  window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
290
  window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
291
  window.PLOT_DEPTH = {json.dumps(PLOT_DEPTH)};
292
  console.log("Streamlit State Injected:", {{
293
  selectedObject: window.SELECTED_OBJECT_TYPE,
294
  initialObjectsCount: window.ALL_INITIAL_OBJECTS ? window.ALL_INITIAL_OBJECTS.length : 0,
295
- plotCount: window.PLOTS_METADATA ? window.PLOTS_METADATA.length : 0,
296
- plotWidth: window.PLOT_WIDTH,
297
- plotDepth: window.PLOT_DEPTH
298
  }});
299
  </script>
300
  """
301
  html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
302
 
303
  # --- Embed HTML Component ---
304
- components.html(
305
- html_content_with_state,
306
- height=750,
307
- scrolling=False
308
- )
309
 
310
  except FileNotFoundError:
311
  st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
312
- st.warning(f"Make sure `{html_file_path}` is in the same directory as `app.py` and `{SAVE_DIR}` exists.")
313
  except Exception as e:
314
- st.error(f"An critical error occurred during HTML preparation or component rendering: {e}")
315
  st.exception(e)
 
6
  import pandas as pd
7
  import uuid
8
  import math # For floor function
 
9
  from streamlit_js_eval import streamlit_js_eval # For JS communication
10
 
11
  # --- Constants ---
12
  SAVE_DIR = "saved_worlds"
13
  PLOT_WIDTH = 50.0 # Width of each plot in 3D space
14
+ PLOT_DEPTH = 50.0 # Depth of each plot
15
  CSV_COLUMNS = ['obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'rot_z', 'rot_order']
16
 
17
  # --- Ensure Save Directory Exists ---
 
19
 
20
  # --- Helper Functions ---
21
 
22
+ # Cache key now depends on refresh_trigger to force reload when counter changes
23
+ @st.cache_data(ttl=3600)
24
+ def load_plot_metadata(refresh_trigger):
25
  """Scans save dir for plot_X*_Z*.csv, sorts, calculates metadata."""
26
+ # This function depends on refresh_trigger only to invalidate cache.
27
+ # The value of refresh_trigger itself isn't used inside.
28
+ st.write(f"Cache Trigger: {refresh_trigger}. Reloading plot metadata...") # Debug log
29
+
30
  plots = []
31
  plot_files = []
 
32
  try:
33
  plot_files = sorted([f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")])
 
34
  except FileNotFoundError:
35
  st.error(f"Save directory '{SAVE_DIR}' not found.")
36
+ return [] # Return empty list on error
37
  except Exception as e:
38
  st.error(f"Error listing save directory '{SAVE_DIR}': {e}")
39
  return []
40
 
 
41
  parsed_plots = []
42
  for filename in plot_files:
43
  try:
44
+ parts = filename[:-4].split('_')
45
+ grid_x = int(parts[1][1:])
46
+ grid_z = int(parts[2][1:])
47
+ # Default name is coordinates, override if name parts exist
48
  plot_name = " ".join(parts[3:]) if len(parts) > 3 else f"Plot ({grid_x},{grid_z})"
49
 
50
  parsed_plots.append({
51
+ 'id': filename[:-4],
52
  'filename': filename,
53
+ 'grid_x': grid_x, 'grid_z': grid_z, 'name': plot_name,
 
 
54
  'x_offset': grid_x * PLOT_WIDTH,
55
+ 'z_offset': grid_z * PLOT_DEPTH
56
  })
57
  except (IndexError, ValueError):
58
  st.warning(f"Could not parse grid coordinates from filename: {filename}. Skipping.")
59
  continue
60
 
 
61
  parsed_plots.sort(key=lambda p: (p['grid_x'], p['grid_z']))
62
+ st.write(f"Metadata loaded for {len(parsed_plots)} plots.") # Debug log
63
  return parsed_plots
64
 
65
+ # No caching needed for loading objects, should always load fresh based on metadata
66
  def load_plot_objects(filename, x_offset, z_offset):
67
  """Loads objects from a CSV, applying the plot's world offsets."""
68
  file_path = os.path.join(SAVE_DIR, filename)
69
  objects = []
70
  try:
71
  df = pd.read_csv(file_path)
 
72
  if not all(col in df.columns for col in ['type', 'pos_x', 'pos_y', 'pos_z']):
73
+ return [] # Skip files missing essential columns silently now
 
 
74
  df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in range(len(df))]))
75
  for col, default in [('rot_x', 0.0), ('rot_y', 0.0), ('rot_z', 0.0), ('rot_order', 'XYZ')]:
76
  if col not in df.columns: df[col] = default
77
 
78
  for _, row in df.iterrows():
79
  obj_data = row.to_dict()
 
80
  obj_data['pos_x'] += x_offset
81
+ obj_data['pos_z'] += z_offset
82
  objects.append(obj_data)
83
  return objects
84
+ except FileNotFoundError: return [] # File might have been deleted between metadata load and object load
85
+ except pd.errors.EmptyDataError: return [] # Empty file is okay
 
 
 
86
  except Exception as e:
87
  st.error(f"Error loading objects from {filename}: {e}")
88
  return []
89
 
90
+ # Overwrites the target file with the provided objects
91
  def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
92
+ """Saves object data list to a CSV, making positions relative. Overwrites file."""
93
  file_path = os.path.join(SAVE_DIR, filename)
94
  relative_objects = []
95
  if not isinstance(objects_data_list, list):
 
100
  pos = obj.get('position', {})
101
  rot = obj.get('rotation', {})
102
  obj_type = obj.get('type', 'Unknown')
103
+ # Use ID provided by JS if available (important for consistency if JS tracks it)
104
  obj_id = obj.get('obj_id', str(uuid.uuid4()))
105
 
106
  if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
 
118
  relative_objects.append(relative_obj)
119
 
120
  try:
121
+ # Overwrite the file with the new data (or empty if list is empty)
122
  df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
123
  df.to_csv(file_path, index=False)
124
+ # st.success(f"Saved/Updated {len(relative_objects)} objects to {filename}") # Success message in calling block
 
125
  return True
126
  except Exception as e:
127
  st.error(f"Failed to save plot data to {filename}: {e}")
128
  return False
129
 
130
  # --- Page Config ---
131
+ st.set_page_config( page_title="Synced World Builder", layout="wide")
132
 
133
  # --- Initialize Session State ---
134
  if 'selected_object' not in st.session_state: st.session_state.selected_object = 'None'
 
135
  if 'js_save_data_result' not in st.session_state: st.session_state.js_save_data_result = None
136
+ # Counter for forcing cache refresh
137
+ if 'refresh_counter' not in st.session_state: st.session_state.refresh_counter = 0
138
 
139
+ # --- Load Plot Metadata (using refresh counter) ---
140
+ plots_metadata = load_plot_metadata(st.session_state.refresh_counter)
 
 
141
 
142
  # --- Load ALL Objects for Rendering ---
143
  all_initial_objects = []
144
+ # st.write(f"Loading objects for {len(plots_metadata)} plots...") # Less verbose now
145
  for plot in plots_metadata:
 
146
  all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
147
+ # st.write(f"Total objects loaded: {len(all_initial_objects)}")
 
148
 
149
  # --- Sidebar ---
150
  with st.sidebar:
151
  st.title("🏗️ World Controls")
152
 
153
+ # *** Refresh Button Logic ***
154
  if st.button("🔄 Refresh World View", key="refresh_button"):
155
+ st.session_state.refresh_counter += 1 # Increment counter
156
+ load_plot_metadata.clear() # Clear cache explicitly
157
+ st.info(f"Refresh triggered (Counter: {st.session_state.refresh_counter}). Reloading...")
158
+ st.rerun() # Force rerun with new counter value
159
 
160
  st.header("Navigation (Plots)")
161
  st.caption("Click to teleport player to a plot.")
162
+ max_cols = 2
163
  cols = st.columns(max_cols)
164
  col_idx = 0
 
165
  sorted_plots_for_nav = sorted(plots_metadata, key=lambda p: (p['grid_x'], p['grid_z']))
166
  for plot in sorted_plots_for_nav:
167
  button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
168
  if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
169
+ target_x = plot['x_offset'] + PLOT_WIDTH / 2 # Center X
170
+ target_z = plot['z_offset'] + PLOT_DEPTH / 2 # Center Z
 
171
  try:
172
  js_code = f"teleportPlayer({target_x}, {target_z});"
173
  streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
174
+ except Exception as e: st.error(f"Teleport command failed: {e}")
 
175
  col_idx = (col_idx + 1) % max_cols
176
 
177
  st.markdown("---")
 
185
  )
186
  if selected_object_type_widget != st.session_state.selected_object:
187
  st.session_state.selected_object = selected_object_type_widget
188
+ # Rerun will update JS via injection, sessionStorage handles unsaved items
189
 
190
  st.markdown("---")
191
 
192
  # --- Saving ---
193
  st.header("Save Work")
194
+ st.caption("Saves ALL objects currently within the player's plot, overwriting previous save for that plot.")
195
+ if st.button("💾 Save Current Plot", key="save_button"): # Renamed button slightly
196
+ # JS func now sends ALL objects in current plot + player position
197
+ js_get_data_code = "getSaveDataAndPosition();"
198
+ streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
199
+ st.rerun()
200
 
201
 
202
  # --- Process Save Data ---
203
  save_data_from_js = st.session_state.get("js_save_processor", None)
204
 
205
+ if save_data_from_js is not None:
206
+ st.info("Processing save request...")
207
  save_processed_successfully = False
208
  try:
209
  payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
210
 
211
  if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
212
  player_pos = payload['playerPosition']
213
+ # These are ALL objects JS found within the target plot boundaries
214
  objects_to_save = payload['objectsToSave']
215
 
216
+ if isinstance(objects_to_save, list):
217
  # Determine target plot based on player position
218
  target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
219
  target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
 
222
  target_plot_x_offset = target_grid_x * PLOT_WIDTH
223
  target_plot_z_offset = target_grid_z * PLOT_DEPTH
224
 
225
+ st.write(f"Saving plot: {target_filename} (Player at: x={player_pos.get('x', 0):.1f}, z={player_pos.get('z', 0):.1f})")
226
  is_new_plot_file = not os.path.exists(os.path.join(SAVE_DIR, target_filename))
227
 
228
+ # --- Save/Overwrite the data ---
229
  save_ok = save_plot_data(target_filename, objects_to_save, target_plot_x_offset, target_plot_z_offset)
230
 
231
  if save_ok:
232
+ # Clear cache so next reload/refresh sees the change
233
+ load_plot_metadata.clear()
234
+ # No need to call JS reset function anymore
235
+ if is_new_plot_file: st.success(f"New plot created/saved: {target_filename}")
 
 
 
236
  else: st.success(f"Updated existing plot: {target_filename}")
237
  save_processed_successfully = True
238
  else:
239
  st.error(f"Failed to save plot data to file: {target_filename}")
240
  else: st.error("Invalid 'objectsToSave' format (expected list).")
241
+ else: st.error("Invalid save payload structure received (missing keys).")
242
 
243
  except json.JSONDecodeError:
244
  st.error("Failed to decode save data from client.")
 
247
  st.error(f"Error processing save: {e}")
248
  st.exception(e)
249
 
250
+ # Clear the trigger data from session state
251
  st.session_state.js_save_processor = None
252
+ # Rerun after processing save to reload the world state
253
  if save_processed_successfully:
254
+ st.session_state.refresh_counter += 1 # Increment counter to ensure cache miss on THIS rerun
255
  st.rerun()
256
 
257
 
258
  # --- Main Area ---
259
+ st.header("Synced Infinite 3D World")
260
+ st.caption(f"Saving to '{SAVE_DIR}'. Plots loaded: {len(plots_metadata)}. Use 'Refresh' button to see others' saves.")
261
 
262
  # --- Load and Prepare HTML ---
263
  html_file_path = 'index.html'
264
+ html_content_with_state = None
265
 
266
  try:
267
  with open(html_file_path, 'r', encoding='utf-8') as f:
268
  html_template = f.read()
269
 
 
 
270
  js_injection_script = f"""
271
  <script>
272
+ // Inject necessary data for JS initialization
273
  window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)};
274
+ window.PLOTS_METADATA = {json.dumps(plots_metadata)}; // Needed for ground generation logic
275
  window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
276
  window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
277
  window.PLOT_DEPTH = {json.dumps(PLOT_DEPTH)};
278
  console.log("Streamlit State Injected:", {{
279
  selectedObject: window.SELECTED_OBJECT_TYPE,
280
  initialObjectsCount: window.ALL_INITIAL_OBJECTS ? window.ALL_INITIAL_OBJECTS.length : 0,
281
+ plotCount: window.PLOTS_METADATA ? window.PLOTS_METADATA.length : 0
 
 
282
  }});
283
  </script>
284
  """
285
  html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
286
 
287
  # --- Embed HTML Component ---
288
+ components.html( html_content_with_state, height=750, scrolling=False )
 
 
 
 
289
 
290
  except FileNotFoundError:
291
  st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
292
+ st.warning(f"Make sure `{html_file_path}` is nearby and `{SAVE_DIR}` exists.")
293
  except Exception as e:
294
+ st.error(f"An critical error occurred during HTML prep/render: {e}")
295
  st.exception(e)