3DWorldBuilder / app.py
awacke1's picture
Update app.py
9f9622d verified
raw
history blame contribute delete
14.3 kB
import streamlit as st # 🌐 Streamlit magic
import streamlit.components.v1 as components # πŸ–ΌοΈ Embed custom HTML/JS
import os # πŸ“‚ File operations
import json # πŸ”„ JSON encoding/decoding
import pandas as pd # πŸ“Š DataFrame handling
import uuid # πŸ†” Unique IDs
import math # βž— Math utils
import time # ⏳ Time utilities
from gamestate import GameState # πŸ’Ό Shared game-state singleton
# πŸš€ Page setup
st.set_page_config(page_title="Infinite World Builder", layout="wide")
# πŸ“ Constants for world dimensions & CSV schema
SAVE_DIR = "saved_worlds"
PLOT_WIDTH = 50.0 # ↔️ Plot width in world units
PLOT_DEPTH = 50.0 # ↕️ Plot depth in world units
CSV_COLUMNS = [
'obj_id', 'type',
'pos_x', 'pos_y', 'pos_z',
'rot_x', 'rot_y', 'rot_z', 'rot_order'
]
# 🎨 Model categories and types
MODEL_CATEGORIES = {
"None": ["None"],
"Buildings": [
"Simple House",
"Cyberpunk Wall Panel",
"Modular Hab Block",
"MegaCorp Skyscraper",
"Castle Wall Section",
"Wooden Door",
"House Roof Section",
"Concrete Bunker Wall",
"Damaged House Facade"
],
"Nature": [
"Tree",
"Rock",
"Pine Tree",
"Boulder",
"Alien Plant",
"Floating Rock Platform",
"Rubble Pile"
],
"Props": [
"Fence Post",
"Rooftop AC Unit",
"Holographic Window Display",
"Jersey Barrier",
"Oil Drum",
"Canned Food",
"Treasure Chest",
"Wall Torch",
"Bone Pile"
],
"Characters": [
"King Figure",
"Soldier Figure",
"Mage Figure",
"Zombie Figure",
"Survivor Figure",
"Dwarf Miner Figure",
"Undead Knight Figure",
"Hero Figure"
],
"Vehicles": [
"Wooden Cart",
"Ballista",
"Siege Tower",
"Buggy Frame",
"Motorbike",
"Hover Bike",
"APC",
"Sand Boat"
],
"Weapons": [
"Makeshift Machete",
"Pistol Body",
"Scope Attachment",
"Laser Pistol",
"Energy Sword",
"Dwarven Axe",
"Magic Staff"
],
"Effects": [
"Candle Flame",
"Dust Cloud",
"Blood Splat Decal",
"Burning Barrel Fire",
"Warp Tunnel Effect",
"Laser Beam",
"Gold Sparkle",
"Steam Vent"
]
}
# πŸ—‚οΈ Ensure directory for plots exists
os.makedirs(SAVE_DIR, exist_ok=True)
@st.cache_data(ttl=3600) # πŸ•’ Cache for 1h
def load_plot_metadata():
# πŸ” Scan SAVE_DIR for plot CSVs
try:
plot_files = [f for f in os.listdir(SAVE_DIR)
if f.endswith(".csv") and f.startswith("plot_X")]
except FileNotFoundError:
st.error(f"Folder '{SAVE_DIR}' missing! 🚨")
return []
except Exception as e:
st.error(f"Error reading '{SAVE_DIR}': {e}")
return []
parsed = []
for fn in plot_files:
try:
parts = fn[:-4].split('_') # strip .csv
gx = int(parts[1][1:]) # X index
gz = int(parts[2][1:]) # Z index
name = " ".join(parts[3:]) if len(parts)>3 else f"Plot({gx},{gz})"
parsed.append({
'id': fn[:-4],
'filename': fn,
'grid_x': gx,
'grid_z': gz,
'name': name,
'x_offset': gx * PLOT_WIDTH,
'z_offset': gz * PLOT_DEPTH
})
except Exception:
st.warning(f"Skip invalid file: {fn}")
parsed.sort(key=lambda p: (p['grid_x'], p['grid_z']))
return parsed
def load_plot_objects(filename, x_offset, z_offset):
# πŸ“₯ Load objects from a plot CSV and shift by offsets
path = os.path.join(SAVE_DIR, filename)
try:
df = pd.read_csv(path)
# πŸ›‘οΈ Ensure essentials exist
if not all(c in df.columns for c in ['type','pos_x','pos_y','pos_z']):
st.warning(f"Missing cols in {filename} 🧐")
return []
# πŸ†” Guarantee obj_id
df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in df.index]))
# πŸ”„ Fill missing rotation
for col, default in [('rot_x',0.0),('rot_y',0.0),('rot_z',0.0),('rot_order','XYZ')]:
if col not in df.columns:
df[col] = default
objs = []
for _, row in df.iterrows():
o = row.to_dict()
o['pos_x'] += x_offset
o['pos_z'] += z_offset
objs.append(o)
return objs
except FileNotFoundError:
st.error(f"CSV not found: {filename}")
return []
except pd.errors.EmptyDataError:
return []
except Exception as e:
st.error(f"Load error {filename}: {e}")
return []
def save_plot_data(filename, objects_list, px, pz):
# πŸ’Ύ Save list of new objects relative to plot origin
path = os.path.join(SAVE_DIR, filename)
if not isinstance(objects_list, list):
st.error("πŸ‘Ž Invalid data format for save")
return False
rel = []
for o in objects_list:
pos = o.get('position', {})
rot = o.get('rotation', {})
typ = o.get('type','Unknown')
oid = o.get('obj_id', str(uuid.uuid4()))
# πŸ›‘ Skip bad objects
if not all(k in pos for k in ['x','y','z']) or typ=='Unknown':
continue
rel.append({
'obj_id': oid, 'type': typ,
'pos_x': pos['x']-px, 'pos_y': pos['y'], 'pos_z': pos['z']-pz,
'rot_x': rot.get('_x',0.0), 'rot_y': rot.get('_y',0.0),
'rot_z': rot.get('_z',0.0), 'rot_order': rot.get('_order','XYZ')
})
try:
pd.DataFrame(rel, columns=CSV_COLUMNS).to_csv(path, index=False)
st.success(f"πŸŽ‰ Saved {len(rel)} to {filename}")
return True
except Exception as e:
st.error(f"Save failed: {e}")
return False
# πŸ”’ Singleton for global world state
@st.cache_resource
def get_game_state():
return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv")
game_state = get_game_state()
# 🧠 Session state defaults
st.session_state.setdefault('selected_object','None')
st.session_state.setdefault('selected_category','None')
st.session_state.setdefault('new_plot_name','')
st.session_state.setdefault('js_save_data_result',None)
st.session_state.setdefault('custom_scale', 1.0)
st.session_state.setdefault('custom_rotation_y', 0)
# πŸ”„ Load everything
plots_metadata = load_plot_metadata()
all_initial_objects = []
for p in plots_metadata:
all_initial_objects += load_plot_objects(p['filename'], p['x_offset'], p['z_offset'])
# πŸ–₯️ Sidebar UI
with st.sidebar:
st.title("πŸ—οΈ World Controls")
st.header("πŸ“ Navigate Plots")
cols = st.columns(2)
i = 0
for p in sorted(plots_metadata, key=lambda x:(x['grid_x'],x['grid_z'])):
label = f"➑️ {p['name']} ({p['grid_x']},{p['grid_z']})"
if cols[i].button(label, key=f"nav_{p['id']}"):
try:
from streamlit_js_eval import streamlit_js_eval
js = f"teleportPlayer({p['x_offset']+PLOT_WIDTH/2},{p['z_offset']+PLOT_DEPTH/2});"
streamlit_js_eval(js_code=js, key=f"tp_{p['id']}")
except Exception as e:
st.error(f"TP fail: {e}")
i = (i+1)%2
st.markdown("---")
st.header("🌲 Place Objects")
# πŸ“‚ Category selector
category_list = list(MODEL_CATEGORIES.keys())
selected_category = st.selectbox(
"Category:",
category_list,
index=category_list.index(st.session_state.selected_category) if st.session_state.selected_category in category_list else 0,
key="selected_category_widget"
)
if selected_category != st.session_state.selected_category:
st.session_state.selected_category = selected_category
if st.session_state.selected_category != "None":
# Default to first item in the new category
st.session_state.selected_object = MODEL_CATEGORIES[selected_category][0]
# πŸ–ΌοΈ Object selector within the category
if selected_category in MODEL_CATEGORIES:
object_list = MODEL_CATEGORIES[selected_category]
current_object = st.session_state.selected_object
if current_object not in object_list:
current_object = object_list[0]
selected_object = st.selectbox(
"Select:",
object_list,
index=object_list.index(current_object) if current_object in object_list else 0,
key="selected_object_widget"
)
if selected_object != st.session_state.selected_object:
st.session_state.selected_object = selected_object
# ↕️ Scale slider (not shown for the None selection)
if st.session_state.selected_object != "None":
st.session_state.custom_scale = st.slider(
"Scale:",
min_value=0.2,
max_value=3.0,
value=st.session_state.custom_scale,
step=0.1,
key="scale_slider"
)
# πŸ”„ Rotation slider (Y-axis)
st.session_state.custom_rotation_y = st.slider(
"Rotation:",
min_value=0,
max_value=359,
value=st.session_state.custom_rotation_y,
step=15,
key="rotation_slider"
)
st.markdown("---")
st.header("πŸ’Ύ Save Work")
if st.button("πŸ’Ύ Save Current Work", key="save_button"):
from streamlit_js_eval import streamlit_js_eval
streamlit_js_eval(js_code="getSaveDataAndPosition();", key="js_save_processor")
st.rerun()
# πŸ“ New plot naming
with st.expander("Create Named Plot"):
st.text_input("New Plot Name:", key="new_plot_name")
if st.button("Create at Current Position"):
if st.session_state.new_plot_name.strip():
from streamlit_js_eval import streamlit_js_eval
streamlit_js_eval(js_code="getSaveDataForNamedPlot();", key="named_plot_processor")
st.rerun()
# πŸ“¨ Handle incoming save data
raw = st.session_state.get("js_save_processor")
if raw:
st.info("πŸ“¬ Got save data!")
ok=False
try:
pay = json.loads(raw) if isinstance(raw,str) else raw
pos, objs = pay.get('playerPosition'), pay.get('objectsToSave')
if isinstance(objs,list) and pos:
gx, gz = math.floor(pos['x']/PLOT_WIDTH), math.floor(pos['z']/PLOT_DEPTH)
fn = f"plot_X{gx}_Z{gz}.csv"
if save_plot_data(fn, objs, gx*PLOT_WIDTH, gz*PLOT_DEPTH):
load_plot_metadata.clear()
try:
from streamlit_js_eval import streamlit_js_eval
streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js")
except:
pass
game_state.update_state(objs)
ok=True
if not ok:
st.error("❌ Save error")
except Exception as e:
st.error(f"Err: {e}")
st.session_state.js_save_processor=None
if ok: st.rerun()
# πŸ“¨ Handle named plot data
named_raw = st.session_state.get("named_plot_processor")
if named_raw:
st.info("πŸ“¬ Got data for named plot!")
ok=False
try:
pay = json.loads(named_raw) if isinstance(named_raw,str) else named_raw
pos, objs = pay.get('playerPosition'), pay.get('objectsToSave')
if isinstance(objs,list) and pos and st.session_state.new_plot_name.strip():
gx, gz = math.floor(pos['x']/PLOT_WIDTH), math.floor(pos['z']/PLOT_DEPTH)
name = st.session_state.new_plot_name.strip().replace(' ', '_')
fn = f"plot_X{gx}_Z{gz}_{name}.csv"
if save_plot_data(fn, objs, gx*PLOT_WIDTH, gz*PLOT_DEPTH):
load_plot_metadata.clear()
try:
from streamlit_js_eval import streamlit_js_eval
streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js")
except:
pass
game_state.update_state(objs)
ok=True
st.session_state.new_plot_name = ""
if not ok:
st.error("❌ Named plot save error")
except Exception as e:
st.error(f"Named plot err: {e}")
st.session_state.named_plot_processor=None
if ok: st.rerun()
# 🏠 Main view
st.header("🌍 Infinite Shared 3D World")
st.caption("➑️ Explore, click to build, πŸ’Ύ to save!")
# πŸ”Œ Inject state into JS
state = {
"ALL_INITIAL_OBJECTS": all_initial_objects,
"PLOTS_METADATA": plots_metadata,
"SELECTED_OBJECT_TYPE": st.session_state.selected_object,
"CUSTOM_SCALE": st.session_state.custom_scale,
"CUSTOM_ROTATION_Y": st.session_state.custom_rotation_y * (math.pi / 180), # Convert to radians
"PLOT_WIDTH": PLOT_WIDTH,
"PLOT_DEPTH": PLOT_DEPTH,
"GAME_STATE": game_state.get_state()
}
try:
with open('index.html','r',encoding='utf-8') as f:
html = f.read()
script = f"""
<script>
window.ALL_INITIAL_OBJECTS = {json.dumps(state['ALL_INITIAL_OBJECTS'])};
window.PLOTS_METADATA = {json.dumps(state['PLOTS_METADATA'])};
window.SELECTED_OBJECT_TYPE = {json.dumps(state['SELECTED_OBJECT_TYPE'])};
window.CUSTOM_SCALE = {json.dumps(state['CUSTOM_SCALE'])};
window.CUSTOM_ROTATION_Y = {json.dumps(state['CUSTOM_ROTATION_Y'])};
window.PLOT_WIDTH = {json.dumps(state['PLOT_WIDTH'])};
window.PLOT_DEPTH = {json.dumps(state['PLOT_DEPTH'])};
window.GAME_STATE = {json.dumps(state['GAME_STATE'])};
console.log('πŸ‘ State injected!', {{
objs: window.ALL_INITIAL_OBJECTS.length,
plots: window.PLOTS_METADATA.length,
gs: window.GAME_STATE.length,
scale: window.CUSTOM_SCALE,
rotation: window.CUSTOM_ROTATION_Y
}});
</script>
"""
html = html.replace('</head>', script + '\n</head>', 1)
components.html(html, height=750, scrolling=False)
except FileNotFoundError:
st.error("❌ index.html missing!")
except Exception as e:
st.error(f"😱 HTML inject failed: {e}")