awacke1's picture
Update app.py
7825ef7 verified
raw
history blame
12.1 kB
# app.py
import streamlit as st
import streamlit.components.v1 as components
import os
import json
import pandas as pd
import uuid
from PIL import Image, ImageDraw # For minimap (using buttons instead now)
from streamlit_js_eval import streamlit_js_eval, sync # For JS communication
# --- Constants ---
SAVE_DIR = "saved_worlds"
PLOT_WIDTH = 50.0 # Width of each plot in 3D space (adjust as needed)
CSV_COLUMNS = ['obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'rot_z', 'rot_order']
# --- Ensure Save Directory Exists ---
os.makedirs(SAVE_DIR, exist_ok=True)
# --- Helper Functions ---
@st.cache_data(ttl=3600) # Cache plot list for an hour, or clear manually
def load_plot_metadata():
"""Scans save dir, sorts plots, calculates metadata."""
plots = []
plot_files = sorted([f for f in os.listdir(SAVE_DIR) if f.endswith(".csv")])
current_x_offset = 0.0
for i, filename in enumerate(plot_files):
# Extract name - assumes format like 'plot_001_MyName.csv' or just 'plot_001.csv'
parts = filename[:-4].split('_') # Remove .csv and split by underscore
plot_id = parts[0] # Assume first part is ID/order
plot_name = " ".join(parts[1:]) if len(parts) > 1 else f"Plot {i+1}"
plots.append({
'id': plot_id,
'name': plot_name,
'filename': filename,
'x_offset': current_x_offset
})
current_x_offset += PLOT_WIDTH
return plots, current_x_offset # Return plots and the next available offset
def load_plot_objects(filename, x_offset):
"""Loads objects from a CSV, applying the plot's x_offset."""
file_path = os.path.join(SAVE_DIR, filename)
objects = []
try:
df = pd.read_csv(file_path)
if not all(col in df.columns for col in CSV_COLUMNS):
st.warning(f"CSV '{filename}' missing expected columns. Skipping.")
return []
for _, row in df.iterrows():
obj_data = row.to_dict()
# Apply world offset
obj_data['pos_x'] += x_offset
objects.append(obj_data)
return objects
except FileNotFoundError:
st.error(f"File not found during object load: {filename}")
return []
except pd.errors.EmptyDataError:
st.info(f"Plot file '{filename}' is empty.")
return [] # Empty file is valid
except Exception as e:
st.error(f"Error loading objects from {filename}: {e}")
return []
def save_plot_data(filename, objects_data_list, plot_x_offset):
"""Saves object data list to a new CSV file, making positions relative."""
file_path = os.path.join(SAVE_DIR, filename)
relative_objects = []
for obj in objects_data_list:
# Ensure required fields exist before proceeding
if not all(k in obj for k in ['position', 'rotation', 'type', 'obj_id']):
print(f"Skipping malformed object during save: {obj}")
continue
relative_obj = {
'obj_id': obj.get('obj_id', str(uuid.uuid4())), # Generate ID if missing
'type': obj.get('type', 'Unknown'),
'pos_x': obj['position'].get('x', 0.0) - plot_x_offset, # Make relative
'pos_y': obj['position'].get('y', 0.0),
'pos_z': obj['position'].get('z', 0.0),
'rot_x': obj['rotation'].get('_x', 0.0),
'rot_y': obj['rotation'].get('_y', 0.0),
'rot_z': obj['rotation'].get('_z', 0.0),
'rot_order': obj['rotation'].get('_order', 'XYZ')
}
relative_objects.append(relative_obj)
try:
df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
df.to_csv(file_path, index=False)
st.success(f"Saved plot data to {filename}")
return True
except Exception as e:
st.error(f"Failed to save plot data to {filename}: {e}")
return False
# --- Page Config ---
st.set_page_config(
page_title="Shared World Builder",
layout="wide"
)
# --- Initialize Session State ---
if 'selected_object' not in st.session_state:
st.session_state.selected_object = 'None'
if 'new_plot_name' not in st.session_state:
st.session_state.new_plot_name = ""
if 'save_request_data' not in st.session_state: # To store data back from JS
st.session_state.save_request_data = None
# --- Load Plot Metadata ---
# Cached function returns list of plots and the next starting x_offset
plots_metadata, next_plot_x_offset = load_plot_metadata()
# --- Load ALL Objects for Rendering ---
# This could be slow with many plots! Consider optimization later.
all_initial_objects = []
for plot in plots_metadata:
all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset']))
# --- Sidebar ---
with st.sidebar:
st.title("🏗️ World Controls")
st.header("Navigation (Plots)")
st.caption("Click to teleport player to the start of a plot.")
# Use columns for a horizontal button layout if desired
cols = st.columns(3)
col_idx = 0
for plot in plots_metadata:
# Use an emoji + name for the button
button_label = f"➡️ {plot.get('name', plot['id'])}"
if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
# Send command to JS to move the player
target_x = plot['x_offset']
streamlit_js_eval(js_code=f"teleportPlayer({target_x});")
# No rerun needed here, JS handles the move
col_idx = (col_idx + 1) % 3 # Cycle through columns
st.markdown("---")
# --- Object Placement ---
st.header("Place Objects")
object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
current_object_index = 0
try:
current_object_index = object_types.index(st.session_state.selected_object)
except ValueError:
st.session_state.selected_object = "None" # Reset if invalid
selected_object_type_widget = st.selectbox(
"Select Object:",
options=object_types,
index=current_object_index,
key="selected_object_widget"
)
if selected_object_type_widget != st.session_state.selected_object:
st.session_state.selected_object = selected_object_type_widget
# No rerun needed just for selection, JS will read state on next placement
st.markdown("---")
# --- Saving ---
st.header("Save New Plot")
st.session_state.new_plot_name = st.text_input(
"Name for New Plot:",
value=st.session_state.new_plot_name,
placeholder="My Awesome Creation"
)
if st.button("💾 Save Current Work as New Plot"):
# 1. Trigger JS function to get data
# This function `getSaveData` should be defined in index.html
# It should collect data for objects considered "new"
# and return them as a JSON string.
# The positions should be WORLD positions.
js_get_data_code = "getSaveData();"
# Use streamlit_js_eval to run JS and get data back into session_state
st.session_state.save_request_data = streamlit_js_eval(
js_code=js_get_data_code,
key="get_save_data" # Unique key for this eval call
# Removed want_output=True, value goes to key if specified
)
# Need to trigger a rerun to process the data AFTER it arrives
# Using sync() might help ensure the value is available immediately after? Let's try without first.
st.rerun() # Rerun to process the returned data below
# --- Process Save Data (if received from JS via key) ---
# This runs AFTER the rerun triggered by the save button
save_data_from_js = st.session_state.get("save_request_data", None)
if save_data_from_js:
st.info("Received save data from client...")
try:
objects_to_save = json.loads(save_data_from_js) # Parse JSON string from JS
if isinstance(objects_to_save, list) and len(objects_to_save) > 0:
# Determine filename for the new plot
new_plot_index = len(plots_metadata) + 1
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}"
new_filename = f"plot_{new_plot_index:03d}_{plot_name_sanitized.replace(' ','_')}.csv"
# Save the data, converting world coords to relative coords inside the func
save_ok = save_plot_data(new_filename, objects_to_save, next_plot_x_offset)
if save_ok:
# IMPORTANT: Clear the plot metadata cache so it reloads with the new file
load_plot_metadata.clear()
# Clear the JS data request state
st.session_state.save_request_data = None
# Reset the new plot name field
st.session_state.new_plot_name = ""
# Reset newly placed objects in JS? Needs another call.
try:
streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js_state")
except Exception as js_e:
st.warning(f"Could not reset JS state after save: {js_e}")
st.success(f"New plot '{plot_name_sanitized}' saved!")
# Rerun AGAIN to reload plots list, redraw minimap, and update JS injection
st.rerun()
else:
st.error("Failed to save plot data to file.")
# Clear the request data even if save failed to prevent retry loop
st.session_state.save_request_data = None
elif isinstance(objects_to_save, list) and len(objects_to_save) == 0:
st.warning("Nothing new to save.")
st.session_state.save_request_data = None # Clear request
else:
st.error(f"Received invalid save data format from client: {type(objects_to_save)}")
st.session_state.save_request_data = None # Clear request
except json.JSONDecodeError:
st.error("Failed to decode save data from client. Data might be corrupted or empty.")
print("Received raw data:", save_data_from_js) # Log for debugging
st.session_state.save_request_data = None # Clear request
except Exception as e:
st.error(f"Error processing save: {e}")
st.exception(e)
st.session_state.save_request_data = None # Clear request
# --- Main Area ---
st.header("Shared 3D World")
st.caption("Build side-by-side with others. Changes load on refresh/save.")
# --- Load and Prepare HTML ---
html_file_path = 'index.html'
html_content_with_state = None # Initialize
try:
with open(html_file_path, 'r', encoding='utf-8') as f:
html_template = f.read()
# --- Inject Python state into JavaScript ---
js_injection_script = f"""
<script>
// Set global variables BEFORE the main script runs
window.ALL_INITIAL_OBJECTS = {json.dumps(all_initial_objects)}; // All objects from all plots
window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
window.PLOT_WIDTH = {json.dumps(PLOT_WIDTH)};
window.NEXT_PLOT_X_OFFSET = {json.dumps(next_plot_x_offset)}; // Needed for save calculation
console.log("Streamlit State Injected:", {{
selectedObject: window.SELECTED_OBJECT_TYPE,
initialObjectsCount: window.ALL_INITIAL_OBJECTS ? window.ALL_INITIAL_OBJECTS.length : 0,
plotWidth: window.PLOT_WIDTH,
nextPlotX: window.NEXT_PLOT_X_OFFSET
}});
</script>
"""
html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
# --- Embed HTML Component ---
components.html(
html_content_with_state,
height=750,
scrolling=False
)
except FileNotFoundError:
st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
st.warning(f"Make sure `{html_file_path}` is in the same directory as `app.py` and `{SAVE_DIR}` exists.")
except Exception as e:
st.error(f"An critical error occurred during HTML preparation or component rendering: {e}")
st.exception(e)