awacke1 commited on
Commit
dfe769e
·
verified ·
1 Parent(s): 7521dc5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -85
app.py CHANGED
@@ -6,7 +6,8 @@ import json
6
  import pandas as pd
7
  import uuid
8
  from PIL import Image, ImageDraw # For minimap (using buttons instead now)
9
- from streamlit_js_eval import streamlit_js_eval, sync # For JS communication
 
10
 
11
  # --- Constants ---
12
  SAVE_DIR = "saved_worlds"
@@ -22,23 +23,25 @@ os.makedirs(SAVE_DIR, exist_ok=True)
22
  def load_plot_metadata():
23
  """Scans save dir, sorts plots, calculates metadata."""
24
  plots = []
 
25
  plot_files = sorted([f for f in os.listdir(SAVE_DIR) if f.endswith(".csv")])
26
 
27
  current_x_offset = 0.0
28
  for i, filename in enumerate(plot_files):
29
  # Extract name - assumes format like 'plot_001_MyName.csv' or just 'plot_001.csv'
30
  parts = filename[:-4].split('_') # Remove .csv and split by underscore
31
- plot_id = parts[0] # Assume first part is ID/order
32
- plot_name = " ".join(parts[1:]) if len(parts) > 1 else f"Plot {i+1}"
33
 
34
  plots.append({
35
- 'id': plot_id,
36
  'name': plot_name,
37
  'filename': filename,
38
  'x_offset': current_x_offset
39
  })
40
  current_x_offset += PLOT_WIDTH
41
- return plots, current_x_offset # Return plots and the next available offset
 
42
 
43
  def load_plot_objects(filename, x_offset):
44
  """Loads objects from a CSV, applying the plot's x_offset."""
@@ -46,22 +49,31 @@ def load_plot_objects(filename, x_offset):
46
  objects = []
47
  try:
48
  df = pd.read_csv(file_path)
49
- if not all(col in df.columns for col in CSV_COLUMNS):
50
- st.warning(f"CSV '{filename}' missing expected columns. Skipping.")
 
51
  return []
 
 
 
 
 
 
 
52
 
53
  for _, row in df.iterrows():
54
  obj_data = row.to_dict()
55
- # Apply world offset
56
  obj_data['pos_x'] += x_offset
57
  objects.append(obj_data)
58
  return objects
59
  except FileNotFoundError:
 
60
  st.error(f"File not found during object load: {filename}")
61
  return []
62
  except pd.errors.EmptyDataError:
63
- st.info(f"Plot file '{filename}' is empty.")
64
- return [] # Empty file is valid
65
  except Exception as e:
66
  st.error(f"Error loading objects from {filename}: {e}")
67
  return []
@@ -71,29 +83,46 @@ def save_plot_data(filename, objects_data_list, plot_x_offset):
71
  """Saves object data list to a new CSV file, making positions relative."""
72
  file_path = os.path.join(SAVE_DIR, filename)
73
  relative_objects = []
 
 
 
 
 
 
74
  for obj in objects_data_list:
75
- # Ensure required fields exist before proceeding
76
- if not all(k in obj for k in ['position', 'rotation', 'type', 'obj_id']):
77
- print(f"Skipping malformed object during save: {obj}")
 
 
 
 
 
78
  continue
79
 
80
  relative_obj = {
81
- 'obj_id': obj.get('obj_id', str(uuid.uuid4())), # Generate ID if missing
82
- 'type': obj.get('type', 'Unknown'),
83
- 'pos_x': obj['position'].get('x', 0.0) - plot_x_offset, # Make relative
84
- 'pos_y': obj['position'].get('y', 0.0),
85
- 'pos_z': obj['position'].get('z', 0.0),
86
- 'rot_x': obj['rotation'].get('_x', 0.0),
87
- 'rot_y': obj['rotation'].get('_y', 0.0),
88
- 'rot_z': obj['rotation'].get('_z', 0.0),
89
- 'rot_order': obj['rotation'].get('_order', 'XYZ')
90
  }
91
  relative_objects.append(relative_obj)
92
 
93
  try:
94
- df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
95
- df.to_csv(file_path, index=False)
96
- st.success(f"Saved plot data to {filename}")
 
 
 
 
 
 
97
  return True
98
  except Exception as e:
99
  st.error(f"Failed to save plot data to {filename}: {e}")
@@ -110,15 +139,16 @@ if 'selected_object' not in st.session_state:
110
  st.session_state.selected_object = 'None'
111
  if 'new_plot_name' not in st.session_state:
112
  st.session_state.new_plot_name = ""
113
- if 'save_request_data' not in st.session_state: # To store data back from JS
114
- st.session_state.save_request_data = None
 
115
 
116
  # --- Load Plot Metadata ---
117
  # Cached function returns list of plots and the next starting x_offset
118
  plots_metadata, next_plot_x_offset = load_plot_metadata()
119
 
120
  # --- Load ALL Objects for Rendering ---
121
- # This could be slow with many plots! Consider optimization later.
122
  all_initial_objects = []
123
  for plot in plots_metadata:
124
  all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset']))
@@ -131,18 +161,20 @@ with st.sidebar:
131
  st.caption("Click to teleport player to the start of a plot.")
132
 
133
  # Use columns for a horizontal button layout if desired
134
- cols = st.columns(3)
 
135
  col_idx = 0
136
  for plot in plots_metadata:
137
  # Use an emoji + name for the button
138
- button_label = f"➡️ {plot.get('name', plot['id'])}"
139
- if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
 
140
  # Send command to JS to move the player
141
  target_x = plot['x_offset']
142
  streamlit_js_eval(js_code=f"teleportPlayer({target_x});")
143
- # No rerun needed here, JS handles the move
144
 
145
- col_idx = (col_idx + 1) % 3 # Cycle through columns
146
 
147
  st.markdown("---")
148
 
@@ -151,126 +183,140 @@ with st.sidebar:
151
  object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
152
  current_object_index = 0
153
  try:
 
154
  current_object_index = object_types.index(st.session_state.selected_object)
155
  except ValueError:
156
- st.session_state.selected_object = "None" # Reset if invalid
 
157
 
158
  selected_object_type_widget = st.selectbox(
159
  "Select Object:",
160
  options=object_types,
161
  index=current_object_index,
162
- key="selected_object_widget"
163
  )
 
164
  if selected_object_type_widget != st.session_state.selected_object:
165
  st.session_state.selected_object = selected_object_type_widget
166
- # No rerun needed just for selection, JS will read state on next placement
 
167
 
168
 
169
  st.markdown("---")
170
 
171
  # --- Saving ---
172
  st.header("Save New Plot")
 
173
  st.session_state.new_plot_name = st.text_input(
174
  "Name for New Plot:",
175
  value=st.session_state.new_plot_name,
176
- placeholder="My Awesome Creation"
 
177
  )
178
 
179
- if st.button("💾 Save Current Work as New Plot"):
180
- # 1. Trigger JS function to get data
181
- # This function `getSaveData` should be defined in index.html
182
- # It should collect data for objects considered "new"
183
- # and return them as a JSON string.
184
- # The positions should be WORLD positions.
185
- js_get_data_code = "getSaveData();"
186
- # Use streamlit_js_eval to run JS and get data back into session_state
187
- st.session_state.save_request_data = streamlit_js_eval(
188
- js_code=js_get_data_code,
189
- key="get_save_data" # Unique key for this eval call
190
- # Removed want_output=True, value goes to key if specified
191
  )
192
- # Need to trigger a rerun to process the data AFTER it arrives
193
- # Using sync() might help ensure the value is available immediately after? Let's try without first.
194
- st.rerun() # Rerun to process the returned data below
 
195
 
196
 
197
- # --- Process Save Data (if received from JS via key) ---
198
- # This runs AFTER the rerun triggered by the save button
199
- save_data_from_js = st.session_state.get("save_request_data", None)
200
 
201
- if save_data_from_js:
202
  st.info("Received save data from client...")
 
203
  try:
204
- objects_to_save = json.loads(save_data_from_js) # Parse JSON string from JS
 
 
 
 
 
205
 
206
- if isinstance(objects_to_save, list) and len(objects_to_save) > 0:
 
207
  # Determine filename for the new plot
208
- new_plot_index = len(plots_metadata) + 1
209
- plot_name_sanitized = "".join(c for c in st.session_state.new_plot_name if c.isalnum() or c in (' ', '_')).rstrip() or f"Plot_{new_plot_index}"
210
- new_filename = f"plot_{new_plot_index:03d}_{plot_name_sanitized.replace(' ','_')}.csv"
 
 
 
 
211
 
212
  # Save the data, converting world coords to relative coords inside the func
213
  save_ok = save_plot_data(new_filename, objects_to_save, next_plot_x_offset)
214
 
215
  if save_ok:
216
- # IMPORTANT: Clear the plot metadata cache so it reloads with the new file
217
  load_plot_metadata.clear()
218
- # Clear the JS data request state
219
- st.session_state.save_request_data = None
220
- # Reset the new plot name field
221
  st.session_state.new_plot_name = ""
222
- # Reset newly placed objects in JS? Needs another call.
223
  try:
 
224
  streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js_state")
225
  except Exception as js_e:
226
  st.warning(f"Could not reset JS state after save: {js_e}")
227
 
228
  st.success(f"New plot '{plot_name_sanitized}' saved!")
229
- # Rerun AGAIN to reload plots list, redraw minimap, and update JS injection
230
- st.rerun()
231
  else:
232
  st.error("Failed to save plot data to file.")
233
- # Clear the request data even if save failed to prevent retry loop
234
- st.session_state.save_request_data = None
235
 
236
- elif isinstance(objects_to_save, list) and len(objects_to_save) == 0:
237
- st.warning("Nothing new to save.")
238
- st.session_state.save_request_data = None # Clear request
239
  else:
240
- st.error(f"Received invalid save data format from client: {type(objects_to_save)}")
241
- st.session_state.save_request_data = None # Clear request
242
 
243
 
244
  except json.JSONDecodeError:
245
  st.error("Failed to decode save data from client. Data might be corrupted or empty.")
246
- print("Received raw data:", save_data_from_js) # Log for debugging
247
- st.session_state.save_request_data = None # Clear request
248
  except Exception as e:
249
  st.error(f"Error processing save: {e}")
250
  st.exception(e)
251
- st.session_state.save_request_data = None # Clear request
 
 
 
 
 
 
 
252
 
253
 
254
  # --- Main Area ---
255
  st.header("Shared 3D World")
256
- st.caption("Build side-by-side with others. Changes load on refresh/save.")
257
 
258
  # --- Load and Prepare HTML ---
259
  html_file_path = 'index.html'
260
  html_content_with_state = None # Initialize
261
 
262
  try:
 
263
  with open(html_file_path, 'r', encoding='utf-8') as f:
264
  html_template = f.read()
265
 
266
- # --- Inject Python state into JavaScript ---
 
267
  js_injection_script = f"""
268
  <script>
269
  // Set global variables BEFORE the main script runs
270
  window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)}; // All objects from all plots
271
  window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
272
  window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
273
- window.NEXT_PLOT_X_OFFSET = {json.dumps(next_plot_x_offset)}; // Needed for save calculation
 
274
  console.log("Streamlit State Injected:", {{
275
  selectedObject: window.SELECTED_OBJECT_TYPE,
276
  initialObjectsCount: window.ALL_INITIAL_OBJECTS ? window.ALL_INITIAL_OBJECTS.length : 0,
@@ -279,18 +325,21 @@ try:
279
  }});
280
  </script>
281
  """
 
 
282
  html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
283
 
284
- # --- Embed HTML Component ---
285
  components.html(
286
  html_content_with_state,
287
- height=750,
288
  scrolling=False
289
  )
290
 
 
291
  except FileNotFoundError:
292
  st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
293
- st.warning(f"Make sure `{html_file_path}` is in the same directory as `app.py` and `{SAVE_DIR}` exists.")
294
  except Exception as e:
295
  st.error(f"An critical error occurred during HTML preparation or component rendering: {e}")
296
- st.exception(e)
 
6
  import pandas as pd
7
  import uuid
8
  from PIL import Image, ImageDraw # For minimap (using buttons instead now)
9
+ # Corrected import: removed 'sync'
10
+ from streamlit_js_eval import streamlit_js_eval # For JS communication
11
 
12
  # --- Constants ---
13
  SAVE_DIR = "saved_worlds"
 
23
  def load_plot_metadata():
24
  """Scans save dir, sorts plots, calculates metadata."""
25
  plots = []
26
+ # Ensure consistent sorting, e.g., alphabetically which often aligns with plot_001, plot_002 etc.
27
  plot_files = sorted([f for f in os.listdir(SAVE_DIR) if f.endswith(".csv")])
28
 
29
  current_x_offset = 0.0
30
  for i, filename in enumerate(plot_files):
31
  # Extract name - assumes format like 'plot_001_MyName.csv' or just 'plot_001.csv'
32
  parts = filename[:-4].split('_') # Remove .csv and split by underscore
33
+ plot_id = filename[:-4] # Use filename (without ext) as a unique ID for now
34
+ plot_name = " ".join(parts[1:]) if len(parts) > 1 else f"Plot {i+1}" # Try to extract name
35
 
36
  plots.append({
37
+ 'id': plot_id, # Use filename as ID
38
  'name': plot_name,
39
  'filename': filename,
40
  'x_offset': current_x_offset
41
  })
42
  current_x_offset += PLOT_WIDTH
43
+ # Also return the offset where the next plot would start
44
+ return plots, current_x_offset
45
 
46
  def load_plot_objects(filename, x_offset):
47
  """Loads objects from a CSV, applying the plot's x_offset."""
 
49
  objects = []
50
  try:
51
  df = pd.read_csv(file_path)
52
+ # Check if required columns exist, handle gracefully if not
53
+ if not all(col in df.columns for col in ['type', 'pos_x', 'pos_y', 'pos_z']):
54
+ st.warning(f"CSV '{filename}' missing essential columns (type, pos_x/y/z). Skipping.")
55
  return []
56
+ # Ensure optional columns default to something sensible if missing
57
+ if 'obj_id' not in df.columns: df['obj_id'] = [str(uuid.uuid4()) for _ in range(len(df))]
58
+ if 'rot_x' not in df.columns: df['rot_x'] = 0.0
59
+ if 'rot_y' not in df.columns: df['rot_y'] = 0.0
60
+ if 'rot_z' not in df.columns: df['rot_z'] = 0.0
61
+ if 'rot_order' not in df.columns: df['rot_order'] = 'XYZ'
62
+
63
 
64
  for _, row in df.iterrows():
65
  obj_data = row.to_dict()
66
+ # Apply world offset accumulated during loading
67
  obj_data['pos_x'] += x_offset
68
  objects.append(obj_data)
69
  return objects
70
  except FileNotFoundError:
71
+ # This shouldn't happen if called via load_plot_metadata results, but handle anyway
72
  st.error(f"File not found during object load: {filename}")
73
  return []
74
  except pd.errors.EmptyDataError:
75
+ # An empty file is valid, represents an empty plot
76
+ return []
77
  except Exception as e:
78
  st.error(f"Error loading objects from {filename}: {e}")
79
  return []
 
83
  """Saves object data list to a new CSV file, making positions relative."""
84
  file_path = os.path.join(SAVE_DIR, filename)
85
  relative_objects = []
86
+ # Ensure objects_data_list is actually a list
87
+ if not isinstance(objects_data_list, list):
88
+ st.error("Invalid data format received for saving (expected a list).")
89
+ print("Invalid save data:", objects_data_list) # Log for debugging
90
+ return False
91
+
92
  for obj in objects_data_list:
93
+ # Validate incoming object structure more carefully
94
+ pos = obj.get('position', {})
95
+ rot = obj.get('rotation', {})
96
+ obj_type = obj.get('type', 'Unknown')
97
+ obj_id = obj.get('obj_id', str(uuid.uuid4())) # Generate ID if missing from JS
98
+
99
+ if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
100
+ print(f"Skipping malformed object during save prep: {obj}")
101
  continue
102
 
103
  relative_obj = {
104
+ 'obj_id': obj_id,
105
+ 'type': obj_type,
106
+ 'pos_x': pos.get('x', 0.0) - plot_x_offset, # Make relative to plot start
107
+ 'pos_y': pos.get('y', 0.0),
108
+ 'pos_z': pos.get('z', 0.0),
109
+ 'rot_x': rot.get('_x', 0.0),
110
+ 'rot_y': rot.get('_y', 0.0),
111
+ 'rot_z': rot.get('_z', 0.0),
112
+ 'rot_order': rot.get('_order', 'XYZ')
113
  }
114
  relative_objects.append(relative_obj)
115
 
116
  try:
117
+ # Only save if there are objects to save
118
+ if relative_objects:
119
+ df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
120
+ df.to_csv(file_path, index=False)
121
+ st.success(f"Saved {len(relative_objects)} objects to {filename}")
122
+ else:
123
+ # Create an empty file with headers if nothing new was placed
124
+ pd.DataFrame(columns=CSV_COLUMNS).to_csv(file_path, index=False)
125
+ st.info(f"Saved empty plot file: {filename}")
126
  return True
127
  except Exception as e:
128
  st.error(f"Failed to save plot data to {filename}: {e}")
 
139
  st.session_state.selected_object = 'None'
140
  if 'new_plot_name' not in st.session_state:
141
  st.session_state.new_plot_name = ""
142
+ # Use a more descriptive key for clarity
143
+ if 'js_save_data_result' not in st.session_state:
144
+ st.session_state.js_save_data_result = None
145
 
146
  # --- Load Plot Metadata ---
147
  # Cached function returns list of plots and the next starting x_offset
148
  plots_metadata, next_plot_x_offset = load_plot_metadata()
149
 
150
  # --- Load ALL Objects for Rendering ---
151
+ # This could be slow with many plots!
152
  all_initial_objects = []
153
  for plot in plots_metadata:
154
  all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset']))
 
161
  st.caption("Click to teleport player to the start of a plot.")
162
 
163
  # Use columns for a horizontal button layout if desired
164
+ max_cols = 3 # Adjust number of columns
165
+ cols = st.columns(max_cols)
166
  col_idx = 0
167
  for plot in plots_metadata:
168
  # Use an emoji + name for the button
169
+ button_label = f"➡️ {plot.get('name', plot['id'])}" # Fallback to id if name missing
170
+ # Use plot filename (unique) as key
171
+ if cols[col_idx].button(button_label, key=f"nav_{plot['filename']}"):
172
  # Send command to JS to move the player
173
  target_x = plot['x_offset']
174
  streamlit_js_eval(js_code=f"teleportPlayer({target_x});")
175
+ # No rerun needed here, JS handles the move instantly
176
 
177
+ col_idx = (col_idx + 1) % max_cols # Cycle through columns
178
 
179
  st.markdown("---")
180
 
 
183
  object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
184
  current_object_index = 0
185
  try:
186
+ # Ensure robustness if selected_object is somehow not in list
187
  current_object_index = object_types.index(st.session_state.selected_object)
188
  except ValueError:
189
+ st.session_state.selected_object = "None" # Reset to default
190
+ current_object_index = 0
191
 
192
  selected_object_type_widget = st.selectbox(
193
  "Select Object:",
194
  options=object_types,
195
  index=current_object_index,
196
+ key="selected_object_widget" # Use a distinct key for the widget
197
  )
198
+ # Update session state only if the widget's value actually changes
199
  if selected_object_type_widget != st.session_state.selected_object:
200
  st.session_state.selected_object = selected_object_type_widget
201
+ # Trigger a targeted JS update if needed, or rely on injection next cycle
202
+ # No rerun needed just for selection if JS reads global state on demand
203
 
204
 
205
  st.markdown("---")
206
 
207
  # --- Saving ---
208
  st.header("Save New Plot")
209
+ # Ensure text input reflects current state value
210
  st.session_state.new_plot_name = st.text_input(
211
  "Name for New Plot:",
212
  value=st.session_state.new_plot_name,
213
+ placeholder="My Awesome Creation",
214
+ key="new_plot_name_input" # Use distinct key
215
  )
216
 
217
+ if st.button("💾 Save Current Work as New Plot", key="save_button"):
218
+ # 1. Trigger JS function `getSaveData()` defined in index.html
219
+ # This function collects data for newly placed objects and returns a JSON string.
220
+ # The key argument ('js_save_processor') stores the JS result in session_state.
221
+ streamlit_js_eval(
222
+ js_code="getSaveData();",
223
+ key="js_save_processor" # Store result under this key
 
 
 
 
 
224
  )
225
+ # Small delay MAY sometimes help ensure the value is set before rerun, but usually not needed
226
+ # import time
227
+ # time.sleep(0.1)
228
+ st.rerun() # Rerun to process the result in the next step
229
 
230
 
231
+ # --- Process Save Data (if received from JS via the key) ---
232
+ # Check the session state key set by the streamlit_js_eval call
233
+ save_data_from_js = st.session_state.get("js_save_processor", None)
234
 
235
+ if save_data_from_js is not None: # Process only if data is present
236
  st.info("Received save data from client...")
237
+ save_processed_successfully = False
238
  try:
239
+ # Ensure data is treated as a string before loading json
240
+ if isinstance(save_data_from_js, str):
241
+ objects_to_save = json.loads(save_data_from_js)
242
+ else:
243
+ # Handle case where it might already be parsed by chance (less likely)
244
+ objects_to_save = save_data_from_js
245
 
246
+ # Proceed only if we have a list (even an empty one is ok now)
247
+ if isinstance(objects_to_save, list):
248
  # Determine filename for the new plot
249
+ new_plot_index = len(plots_metadata) # 0-based index -> number of plots
250
+ # Sanitize name: replace spaces, keep only alphanumeric/underscore
251
+ plot_name_sanitized = "".join(c for c in st.session_state.new_plot_name if c.isalnum() or c in (' ')).strip().replace(' ', '_')
252
+ if not plot_name_sanitized: # Ensure there is a name part
253
+ plot_name_sanitized = f"Plot_{new_plot_index + 1}"
254
+
255
+ new_filename = f"plot_{new_plot_index:03d}_{plot_name_sanitized}.csv"
256
 
257
  # Save the data, converting world coords to relative coords inside the func
258
  save_ok = save_plot_data(new_filename, objects_to_save, next_plot_x_offset)
259
 
260
  if save_ok:
261
+ # Clear the plot metadata cache so it reloads with the new file
262
  load_plot_metadata.clear()
263
+ # Reset the new plot name field for next time
 
 
264
  st.session_state.new_plot_name = ""
265
+ # Reset newly placed objects in JS AFTER successful save
266
  try:
267
+ # This call tells JS to clear its internal 'newlyPlacedObjects' array
268
  streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js_state")
269
  except Exception as js_e:
270
  st.warning(f"Could not reset JS state after save: {js_e}")
271
 
272
  st.success(f"New plot '{plot_name_sanitized}' saved!")
273
+ save_processed_successfully = True
 
274
  else:
275
  st.error("Failed to save plot data to file.")
 
 
276
 
 
 
 
277
  else:
278
+ st.error(f"Received invalid save data format from client (expected list): {type(objects_to_save)}")
 
279
 
280
 
281
  except json.JSONDecodeError:
282
  st.error("Failed to decode save data from client. Data might be corrupted or empty.")
283
+ print("Received raw data:", save_data_from_js) # Log raw data
 
284
  except Exception as e:
285
  st.error(f"Error processing save: {e}")
286
  st.exception(e)
287
+
288
+ # IMPORTANT: Clear the session state key regardless of success/failure
289
+ # to prevent reprocessing on the next rerun unless the button is clicked again.
290
+ st.session_state.js_save_processor = None
291
+
292
+ # Rerun AGAIN after processing save to reflect changes (new plot loaded, cache cleared etc.)
293
+ if save_processed_successfully:
294
+ st.rerun()
295
 
296
 
297
  # --- Main Area ---
298
  st.header("Shared 3D World")
299
+ st.caption("Build side-by-side with others. Saving adds a new plot to the right.")
300
 
301
  # --- Load and Prepare HTML ---
302
  html_file_path = 'index.html'
303
  html_content_with_state = None # Initialize
304
 
305
  try:
306
+ # --- Read the HTML template file ---
307
  with open(html_file_path, 'r', encoding='utf-8') as f:
308
  html_template = f.read()
309
 
310
+ # --- Prepare JavaScript code to inject state ---
311
+ # Ensure all state variables are correctly serialized as JSON
312
  js_injection_script = f"""
313
  <script>
314
  // Set global variables BEFORE the main script runs
315
  window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)}; // All objects from all plots
316
  window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
317
  window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
318
+ window.NEXT_PLOT_X_OFFSET = {json.dumps(next_plot_x_offset)}; // Needed for save calculation & ground size
319
+ // Basic logging to verify state in browser console
320
  console.log("Streamlit State Injected:", {{
321
  selectedObject: window.SELECTED_OBJECT_TYPE,
322
  initialObjectsCount: window.ALL_INITIAL_OBJECTS ? window.ALL_INITIAL_OBJECTS.length : 0,
 
325
  }});
326
  </script>
327
  """
328
+ # --- Inject the script into the HTML template ---
329
+ # Replacing just before </head> is generally safe
330
  html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
331
 
332
+ # --- Embed HTML Component (ONLY if HTML loading and preparation succeeded) ---
333
  components.html(
334
  html_content_with_state,
335
+ height=750, # Adjust height as needed
336
  scrolling=False
337
  )
338
 
339
+ # --- Error Handling ---
340
  except FileNotFoundError:
341
  st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
342
+ st.warning(f"Please make sure `{html_file_path}` is in the same directory as `app.py` and that the `{SAVE_DIR}` directory exists.")
343
  except Exception as e:
344
  st.error(f"An critical error occurred during HTML preparation or component rendering: {e}")
345
+ st.exception(e) # Show full traceback for debugging