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