Spaces:
Sleeping
Sleeping
File size: 11,512 Bytes
2475edf 8ad17fe f2268b5 2475edf f2268b5 2475edf f2268b5 2475edf 8ad17fe f2268b5 2475edf 8ad17fe f2268b5 8ad17fe f2268b5 8ad17fe f2268b5 8ad17fe f2268b5 8ad17fe f2268b5 8ad17fe f2268b5 8ad17fe 2475edf 8ad17fe f2268b5 8ad17fe f2268b5 8ad17fe f2268b5 2475edf 8ad17fe 2475edf 8ad17fe 2475edf f2268b5 f7eafd6 8ad17fe f2268b5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# app.py
import streamlit as st
import streamlit.components.v1 as components
import os
import json
import uuid
from PIL import Image, ImageDraw # For minimap
import urllib.parse # For decoding save data
# --- Constants ---
SAVE_DIR = "saved_worlds"
WORLD_MAP_FILE = os.path.join(SAVE_DIR, "world_map.json")
DEFAULT_SPACE_SIZE = 50 # Matches ground plane size in JS (approx)
MINIMAP_CELL_SIZE = 10 # Pixels per cell in minimap image
# --- Ensure Save Directory Exists ---
os.makedirs(SAVE_DIR, exist_ok=True)
# --- Helper Functions for Persistence ---
def load_world_map():
"""Loads the world map metadata."""
if os.path.exists(WORLD_MAP_FILE):
try:
with open(WORLD_MAP_FILE, 'r') as f:
return json.load(f)
except json.JSONDecodeError:
st.error("Error reading world map file. Starting fresh.")
return {"spaces": {}} # Return empty if corrupt
return {"spaces": {}} # SpaceID -> {"grid_x": int, "grid_y": int, "name": str}
def save_world_map(world_data):
"""Saves the world map metadata."""
with open(WORLD_MAP_FILE, 'w') as f:
json.dump(world_data, f, indent=4)
def save_space_data(space_id, objects_data):
"""Saves the object data for a specific space."""
file_path = os.path.join(SAVE_DIR, f"{space_id}.json")
with open(file_path, 'w') as f:
# Store objects directly, could add metadata later
json.dump({"objects": objects_data}, f, indent=4)
def load_space_data(space_id):
"""Loads object data for a specific space."""
file_path = os.path.join(SAVE_DIR, f"{space_id}.json")
if os.path.exists(file_path):
try:
with open(file_path, 'r') as f:
data = json.load(f)
return data.get("objects", []) # Return objects list or empty
except json.JSONDecodeError:
st.error(f"Error reading space file {space_id}.json.")
return []
return [] # Return empty list if file doesn't exist
def find_next_available_grid_slot(world_data):
"""Finds the next empty slot in a spiral pattern (simple version)."""
occupied = set((d["grid_x"], d["grid_y"]) for d in world_data.get("spaces", {}).values())
x, y = 0, 0
dx, dy = 0, -1
steps = 0
limit = 1
count = 0
while (x, y) in occupied:
if x == y or (x < 0 and x == -y) or (x > 0 and x == 1-y):
dx, dy = -dy, dx # Change direction (spiral)
x, y = x + dx, y + dy
count += 1
if count > 1000: # Safety break
st.error("Could not find empty grid slot easily!")
return None, None
return x, y
# --- Minimap Generation ---
def generate_minimap(world_data, current_space_id=None):
spaces = world_data.get("spaces", {})
if not spaces:
return None # No map if no spaces saved
coords = [(d["grid_x"], d["grid_y"]) for d in spaces.values()]
min_x = min(c[0] for c in coords)
max_x = max(c[0] for c in coords)
min_y = min(c[1] for c in coords)
max_y = max(c[1] for c in coords)
img_width = (max_x - min_x + 1) * MINIMAP_CELL_SIZE
img_height = (max_y - min_y + 1) * MINIMAP_CELL_SIZE
img = Image.new('RGB', (img_width, img_height), color = 'lightgrey')
draw = ImageDraw.Draw(img)
for space_id, data in spaces.items():
cell_x = (data["grid_x"] - min_x) * MINIMAP_CELL_SIZE
cell_y = (data["grid_y"] - min_y) * MINIMAP_CELL_SIZE
color = "blue"
if space_id == current_space_id:
color = "red" # Highlight current space
elif current_space_id is None and space_id == list(spaces.keys())[0]: # Highlight first if none selected
color = "red"
draw.rectangle(
[cell_x, cell_y, cell_x + MINIMAP_CELL_SIZE -1, cell_y + MINIMAP_CELL_SIZE -1],
fill=color, outline="black"
)
return img
# --- Page Config ---
st.set_page_config(
page_title="Multiplayer World Builder",
layout="wide"
)
# --- Initialize Session State ---
if 'selected_object' not in st.session_state:
st.session_state.selected_object = 'None'
if 'current_space_id' not in st.session_state:
st.session_state.current_space_id = None # Will be set when loading/creating
if 'space_name' not in st.session_state:
st.session_state.space_name = ""
if 'initial_objects' not in st.session_state:
st.session_state.initial_objects = [] # Objects to load into JS
# --- Load initial world data ---
world_data = load_world_map()
# --- Handle Save Data from JS (Query Param Workaround) ---
query_params = st.query_params.to_dict()
save_data_encoded = query_params.get("save_data")
save_triggered = False
if save_data_encoded:
try:
save_data_json = urllib.parse.unquote(save_data_encoded[0]) # Get first value if list
objects_to_save = json.loads(save_data_json)
space_id_to_save = query_params.get("space_id", [st.session_state.current_space_id])[0] # Get from URL or state
space_name_to_save = query_params.get("space_name", [st.session_state.space_name])[0]
if not space_id_to_save:
space_id_to_save = str(uuid.uuid4()) # Create new ID
st.session_state.current_space_id = space_id_to_save # Update state
grid_x, grid_y = find_next_available_grid_slot(world_data)
if grid_x is not None:
world_data.setdefault("spaces", {})[space_id_to_save] = {
"grid_x": grid_x,
"grid_y": grid_y,
"name": space_name_to_save or f"Space {len(world_data.get('spaces',{}))+1}"
}
save_world_map(world_data)
else:
st.error("Failed to assign grid position!")
# Save the actual object data
save_space_data(space_id_to_save, objects_to_save)
st.success(f"Space '{space_name_to_save or space_id_to_save}' saved successfully!")
# Update name in world map if it changed and space exists
if space_id_to_save in world_data.get("spaces", {}) and space_name_to_save:
world_data["spaces"][space_id_to_save]["name"] = space_name_to_save
save_world_map(world_data)
# IMPORTANT: Clear query param to prevent resave on refresh
st.query_params.clear()
save_triggered = True # Flag to maybe skip immediate rerun if needed below
except Exception as e:
st.error(f"Error processing save data: {e}")
st.exception(e) # Show traceback
# Need to reload objects if just saved or loading a new space
if 'current_space_id' in st.session_state and st.session_state.current_space_id:
st.session_state.initial_objects = load_space_data(st.session_state.current_space_id)
# --- Sidebar Controls ---
with st.sidebar:
st.title("🏗️ World Controls")
# --- Space Management ---
st.subheader("Manage Spaces")
saved_spaces = list(world_data.get("spaces", {}).items()) # List of (id, data) tuples
space_options = {sid: data.get("name", f"Unnamed ({sid[:6]}...)") for sid, data in saved_spaces}
space_options["_new_"] = "✨ Create New Space ✨" # Special option
selected_space_display = st.selectbox(
"Load or Create Space:",
options = ["_new_"] + list(space_options.keys()),
format_func = lambda x: space_options.get(x, "Select...") if x != "_new_" else "✨ Create New Space ✨",
index=0, # Default to Create New
key="space_selection_key" # Unique key might be needed if dynamically changing options
# Note: Changing this will trigger rerun. Need logic below to handle load.
)
# Handle Load/Create based on selection
if st.session_state.space_selection_key != "_new_":
# Load existing selected
if st.session_state.current_space_id != st.session_state.space_selection_key:
st.session_state.current_space_id = st.session_state.space_selection_key
st.session_state.initial_objects = load_space_data(st.session_state.current_space_id)
st.session_state.space_name = world_data["spaces"][st.session_state.current_space_id].get("name", "")
st.rerun() # Rerun to load data and update JS injection
elif st.session_state.space_selection_key == "_new_" and st.session_state.current_space_id is not None:
# Handle switch from existing to "Create New"
st.session_state.current_space_id = None
st.session_state.initial_objects = []
st.session_state.space_name = ""
st.rerun()
current_name = st.text_input(
"Current Space Name:",
value=st.session_state.space_name,
key="current_space_name_input"
)
# Update state immediately if name changes
if current_name != st.session_state.space_name:
st.session_state.space_name = current_name
# If editing an existing space, maybe update world_map.json immediately or on save? Let's do on save for simplicity.
st.info(f"Current Space ID: {st.session_state.current_space_id or 'None (New)'}")
st.caption("Saving uses the name above. A unique ID is assigned automatically for new spaces.")
st.markdown("---")
# --- Object Placement Controls ---
st.subheader("Place Objects")
object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
selected = st.selectbox(
"Select Object to Place:",
options=object_types,
key='selected_object'
)
st.markdown("---")
# --- Minimap ---
st.subheader("World Minimap")
minimap_img = generate_minimap(world_data, st.session_state.current_space_id)
if minimap_img:
st.image(minimap_img, caption="Blue: Saved Spaces, Red: Current", use_column_width=True)
else:
st.caption("No spaces saved yet.")
# --- Main Area ---
st.header("3D Space Editor")
st.caption(f"Editing: {st.session_state.space_name or 'New Space'}")
# --- Load and Prepare HTML ---
html_file_path = 'index.html'
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.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
window.INITIAL_OBJECTS = {json.dumps(st.session_state.initial_objects)};
window.CURRENT_SPACE_ID = {json.dumps(st.session_state.current_space_id)};
window.CURRENT_SPACE_NAME = {json.dumps(st.session_state.space_name)};
console.log("Streamlit State:", {{
selectedObject: window.SELECTED_OBJECT_TYPE,
initialObjects: window.INITIAL_OBJECTS,
spaceId: window.CURRENT_SPACE_ID,
spaceName: window.CURRENT_SPACE_NAME
}});
</script>
"""
# Insert the injection script just before the closing </head>
# Using placeholder is safer if you modify index.html: 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, # Adjust height as needed
scrolling=False
)
except FileNotFoundError:
st.error(f"Error: Could not find the file '{html_file_path}'.")
st.warning(f"Please make sure `{html_file_path}` is in the same directory as `app.py` and that the `{SAVE_DIR}` directory exists.")
except Exception as e:
st.error(f"An error occurred: {e}")
st.exception(e) |