Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -25,8 +25,10 @@ def load_plot_metadata():
|
|
25 |
"""Scans save dir for plot_X*_Z*.csv, sorts, calculates metadata."""
|
26 |
plots = []
|
27 |
plot_files = []
|
|
|
28 |
try:
|
29 |
-
plot_files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")]
|
|
|
30 |
except FileNotFoundError:
|
31 |
st.error(f"Save directory '{SAVE_DIR}' not found.")
|
32 |
return []
|
@@ -59,7 +61,7 @@ def load_plot_metadata():
|
|
59 |
|
60 |
# Sort primarily by X, then by Z
|
61 |
parsed_plots.sort(key=lambda p: (p['grid_x'], p['grid_z']))
|
62 |
-
|
63 |
return parsed_plots
|
64 |
|
65 |
def load_plot_objects(filename, x_offset, z_offset):
|
@@ -70,7 +72,7 @@ def load_plot_objects(filename, x_offset, z_offset):
|
|
70 |
df = pd.read_csv(file_path)
|
71 |
# Check required columns
|
72 |
if not all(col in df.columns for col in ['type', 'pos_x', 'pos_y', 'pos_z']):
|
73 |
-
st.warning(f"CSV '{filename}' missing essential columns. Skipping.")
|
74 |
return []
|
75 |
# Add defaults for optional columns
|
76 |
df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in range(len(df))]))
|
@@ -124,7 +126,8 @@ def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
|
|
124 |
try:
|
125 |
df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
|
126 |
df.to_csv(file_path, index=False)
|
127 |
-
|
|
|
128 |
return True
|
129 |
except Exception as e:
|
130 |
st.error(f"Failed to save plot data to {filename}: {e}")
|
@@ -135,22 +138,34 @@ st.set_page_config( page_title="Infinite World Builder", layout="wide")
|
|
135 |
|
136 |
# --- Initialize Session State ---
|
137 |
if 'selected_object' not in st.session_state: st.session_state.selected_object = 'None'
|
138 |
-
|
139 |
if 'js_save_data_result' not in st.session_state: st.session_state.js_save_data_result = None
|
140 |
|
141 |
# --- Load Plot Metadata ---
|
142 |
-
#
|
|
|
143 |
plots_metadata = load_plot_metadata()
|
144 |
|
145 |
# --- Load ALL Objects for Rendering ---
|
146 |
all_initial_objects = []
|
|
|
147 |
for plot in plots_metadata:
|
|
|
148 |
all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
|
|
|
|
|
149 |
|
150 |
# --- Sidebar ---
|
151 |
with st.sidebar:
|
152 |
st.title("🏗️ World Controls")
|
153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
st.header("Navigation (Plots)")
|
155 |
st.caption("Click to teleport player to a plot.")
|
156 |
max_cols = 2 # Adjust columns for potentially more buttons
|
@@ -161,11 +176,11 @@ with st.sidebar:
|
|
161 |
for plot in sorted_plots_for_nav:
|
162 |
button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
|
163 |
if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
|
164 |
-
|
165 |
-
|
|
|
166 |
try:
|
167 |
-
|
168 |
-
js_code = f"teleportPlayer({target_x + PLOT_WIDTH/2}, {target_z + PLOT_DEPTH/2});"
|
169 |
streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
|
170 |
except Exception as e:
|
171 |
st.error(f"Failed to send teleport command: {e}")
|
@@ -182,7 +197,7 @@ with st.sidebar:
|
|
182 |
)
|
183 |
if selected_object_type_widget != st.session_state.selected_object:
|
184 |
st.session_state.selected_object = selected_object_type_widget
|
185 |
-
#
|
186 |
|
187 |
st.markdown("---")
|
188 |
|
@@ -191,60 +206,53 @@ with st.sidebar:
|
|
191 |
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.")
|
192 |
if st.button("💾 Save Current Work", key="save_button"):
|
193 |
# Trigger JS to get data AND player position
|
194 |
-
js_get_data_code = "getSaveDataAndPosition();" # JS function
|
195 |
-
streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
|
196 |
st.rerun() # Rerun to process result
|
197 |
|
198 |
|
199 |
# --- Process Save Data ---
|
200 |
save_data_from_js = st.session_state.get("js_save_processor", None)
|
201 |
|
202 |
-
if save_data_from_js is not None:
|
203 |
st.info("Received save data from client...")
|
204 |
save_processed_successfully = False
|
205 |
try:
|
206 |
-
# Expecting { playerPosition: {x,y,z}, objectsToSave: [...] }
|
207 |
payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
|
208 |
|
209 |
if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
|
210 |
player_pos = payload['playerPosition']
|
211 |
objects_to_save = payload['objectsToSave']
|
212 |
|
213 |
-
if isinstance(objects_to_save, list): # Allow saving empty list
|
214 |
# Determine target plot based on player position
|
215 |
target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
|
216 |
-
target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
|
217 |
|
218 |
target_filename = f"plot_X{target_grid_x}_Z{target_grid_z}.csv"
|
219 |
target_plot_x_offset = target_grid_x * PLOT_WIDTH
|
220 |
target_plot_z_offset = target_grid_z * PLOT_DEPTH
|
221 |
|
222 |
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})")
|
223 |
-
|
224 |
-
# Check if this plot already exists in metadata (for logging/future logic)
|
225 |
is_new_plot_file = not os.path.exists(os.path.join(SAVE_DIR, target_filename))
|
226 |
|
|
|
227 |
save_ok = save_plot_data(target_filename, objects_to_save, target_plot_x_offset, target_plot_z_offset)
|
228 |
|
229 |
if save_ok:
|
230 |
-
load_plot_metadata.clear() # Clear cache
|
231 |
try: # Tell JS to clear its unsaved state
|
232 |
streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js_state")
|
233 |
except Exception as js_e:
|
234 |
st.warning(f"Could not reset JS state after save: {js_e}")
|
235 |
|
236 |
-
if is_new_plot_file:
|
237 |
-
|
238 |
-
else:
|
239 |
-
st.success(f"Updated existing plot: {target_filename}")
|
240 |
save_processed_successfully = True
|
241 |
else:
|
242 |
st.error(f"Failed to save plot data to file: {target_filename}")
|
243 |
-
else:
|
244 |
-
|
245 |
-
else:
|
246 |
-
st.error("Invalid save payload structure received from client.")
|
247 |
-
print("Received payload:", payload) # Log for debugging
|
248 |
|
249 |
except json.JSONDecodeError:
|
250 |
st.error("Failed to decode save data from client.")
|
@@ -253,30 +261,31 @@ if save_data_from_js is not None:
|
|
253 |
st.error(f"Error processing save: {e}")
|
254 |
st.exception(e)
|
255 |
|
256 |
-
# Clear the trigger data from session state
|
257 |
st.session_state.js_save_processor = None
|
258 |
-
# Rerun after processing to reflect changes
|
259 |
if save_processed_successfully:
|
260 |
st.rerun()
|
261 |
|
262 |
|
263 |
# --- Main Area ---
|
264 |
st.header("Infinite Shared 3D World")
|
265 |
-
st.caption("
|
266 |
|
267 |
# --- Load and Prepare HTML ---
|
268 |
html_file_path = 'index.html'
|
269 |
-
html_content_with_state = None
|
270 |
|
271 |
try:
|
272 |
with open(html_file_path, 'r', encoding='utf-8') as f:
|
273 |
html_template = f.read()
|
274 |
|
275 |
# --- Inject Python state into JavaScript ---
|
|
|
276 |
js_injection_script = f"""
|
277 |
<script>
|
278 |
window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)};
|
279 |
-
window.PLOTS_METADATA = {json.dumps(plots_metadata)};
|
280 |
window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
|
281 |
window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
|
282 |
window.PLOT_DEPTH = {json.dumps(PLOT_DEPTH)};
|
|
|
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 []
|
|
|
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):
|
|
|
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))]))
|
|
|
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}")
|
|
|
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
|
|
|
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}")
|
|
|
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 |
|
|
|
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)
|
231 |
|
232 |
target_filename = f"plot_X{target_grid_x}_Z{target_grid_z}.csv"
|
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 |
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)};
|