awacke1 commited on
Commit
f2268b5
·
verified ·
1 Parent(s): 006833b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +243 -38
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 # To safely embed the python variable into JS
 
 
 
6
 
7
- # --- 1. Set Page Configuration (Set first!) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  st.set_page_config(
9
- page_title="Three.js Editor",
10
- layout="wide" # Use the full screen width
11
  )
12
 
13
  # --- Initialize Session State ---
14
  if 'selected_object' not in st.session_state:
15
- st.session_state.selected_object = 'None' # Default selection
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  # --- Sidebar Controls ---
18
  with st.sidebar:
19
- st.title("🛠️ Controls & State")
20
- st.caption("Select an object type and click on the ground in the main view to place it.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Selectbox to choose object type - this updates session_state on change
 
 
 
 
 
 
 
26
  selected = st.selectbox(
27
  "Select Object to Place:",
28
  options=object_types,
29
- key='selected_object' # Link to session state key
30
  )
 
31
 
32
- st.write("---")
33
- st.header("Current State:")
34
- st.write(f"Selected Object Type: **{st.session_state.selected_object}**")
35
- # Add more state tracking here later if needed
36
- st.info("Use WASD/Arrows in the main view if player movement is enabled in JS.")
 
 
37
 
38
 
39
- # --- Main Area for the 3D View ---
40
- st.header("🏗️ Three.js Primitive Builder")
 
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 this global variable based on Streamlit state BEFORE the main script runs
54
  window.SELECTED_OBJECT_TYPE = {json.dumps(st.session_state.selected_object)};
55
- console.log("Streamlit selected object:", window.SELECTED_OBJECT_TYPE);
 
 
 
 
 
 
 
 
56
  </script>
57
  """
58
- # Insert the injection script just before the closing </head> or starting <script type="module">
59
- # A simple approach: find the main script tag and insert before it.
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 `index.html` is in the same directory as `app.py`.")
85
  except Exception as e:
86
  st.error(f"An error occurred: {e}")
87
- st.exception(e) # Show full traceback for debugging
 
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)