Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
|
|
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 =
|
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 |
-
|
|
|
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 |
-
|
50 |
-
|
|
|
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 |
-
|
64 |
-
return []
|
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 |
-
#
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
|
|
|
|
78 |
continue
|
79 |
|
80 |
relative_obj = {
|
81 |
-
'obj_id':
|
82 |
-
'type':
|
83 |
-
'pos_x':
|
84 |
-
'pos_y':
|
85 |
-
'pos_z':
|
86 |
-
'rot_x':
|
87 |
-
'rot_y':
|
88 |
-
'rot_z':
|
89 |
-
'rot_order':
|
90 |
}
|
91 |
relative_objects.append(relative_obj)
|
92 |
|
93 |
try:
|
94 |
-
|
95 |
-
|
96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
114 |
-
|
|
|
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!
|
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 |
-
|
|
|
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 |
-
|
|
|
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) %
|
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
|
|
|
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 |
-
#
|
|
|
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
|
181 |
-
# This function
|
182 |
-
#
|
183 |
-
|
184 |
-
|
185 |
-
|
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 |
-
#
|
193 |
-
#
|
194 |
-
|
|
|
195 |
|
196 |
|
197 |
-
# --- Process Save Data (if received from JS via key) ---
|
198 |
-
#
|
199 |
-
save_data_from_js = st.session_state.get("
|
200 |
|
201 |
-
if save_data_from_js:
|
202 |
st.info("Received save data from client...")
|
|
|
203 |
try:
|
204 |
-
|
|
|
|
|
|
|
|
|
|
|
205 |
|
206 |
-
if
|
|
|
207 |
# Determine filename for the new plot
|
208 |
-
new_plot_index = len(plots_metadata)
|
209 |
-
|
210 |
-
|
|
|
|
|
|
|
|
|
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 |
-
#
|
217 |
load_plot_metadata.clear()
|
218 |
-
#
|
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
|
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 |
-
|
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
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
252 |
|
253 |
|
254 |
# --- Main Area ---
|
255 |
st.header("Shared 3D World")
|
256 |
-
st.caption("Build side-by-side with others.
|
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 |
-
# ---
|
|
|
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"
|
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
|