Spaces:
Running
Running
# app.py | |
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 utilities | |
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' | |
] | |
# ποΈ Ensure directory for saved plots | |
os.makedirs(SAVE_DIR, exist_ok=True) | |
# π§ Load prefab kits definitions | |
def load_kits(): | |
try: | |
with open("kits.json", "r", encoding="utf-8") as f: | |
return json.load(f) | |
except FileNotFoundError: | |
st.error("kits.json missing! π¦") | |
return {} | |
except Exception as e: | |
st.error(f"Error loading kits.json: {e}") | |
return {} | |
kits = load_kits() # π name β module list | |
# --- Existing load/save/state functions --- | |
def load_plot_metadata(): | |
try: | |
files = [f for f in os.listdir(SAVE_DIR) if f.endswith('.csv') and f.startswith('plot_X')] | |
except Exception as e: | |
st.error(f"Error scanning {SAVE_DIR}: {e}") | |
return [] | |
parsed = [] | |
for fn in files: | |
try: | |
parts = fn[:-4].split('_') # remove .csv | |
gx = int(parts[1][1:]); gz = int(parts[2][1:]) | |
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: | |
st.warning(f"Skipping 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): | |
path = os.path.join(SAVE_DIR, filename) | |
try: | |
df = pd.read_csv(path) | |
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 [] | |
df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in df.index])) | |
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 _, r in df.iterrows(): | |
o = r.to_dict() | |
o['pos_x'] += x_offset; o['pos_z'] += z_offset | |
objs.append(o) | |
return objs | |
except Exception: | |
return [] | |
def save_plot_data(filename, objects_list, px, pz): | |
path = os.path.join(SAVE_DIR, filename) | |
if not isinstance(objects_list, list): | |
st.error("Invalid save data") | |
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())) | |
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 | |
def get_game_state(): | |
return GameState(save_dir=SAVE_DIR,csv_filename='world_state.csv') | |
game_state=get_game_state() | |
# π§ Session defaults | |
st.session_state.setdefault('selected_object','None') | |
# π Load metadata & objects | |
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 kits | |
with st.sidebar: | |
st.title("ποΈ World Controls") | |
st.header("π Place Kits") | |
options=["None"]+list(kits.keys()) | |
idx=options.index(st.session_state.selected_object) if st.session_state.selected_object in options else 0 | |
sel=st.selectbox("Select Kit:",options,index=idx,key="selected_object_widget") | |
if sel!=st.session_state.selected_object: | |
st.session_state.selected_object=sel | |
st.markdown("---") | |
st.header("πΎ Save Work") | |
if st.button("πΎ Save Current Work"): | |
from streamlit_js_eval import streamlit_js_eval | |
streamlit_js_eval(js_code="getSaveDataAndPosition();",key="js_save_processor") | |
st.rerun() | |
# π Main area | |
st.header("π Infinite Shared 3D World") | |
st.caption("Click to explore & place kits, then πΎ to save!") | |
# π Inject state into JS | |
injected_state={ | |
"ALL_INITIAL_OBJECTS":all_initial_objects, | |
"PLOTS_METADATA":plots_metadata, | |
"SELECTED_OBJECT_TYPE":st.session_state.selected_object, | |
"PLOT_WIDTH":PLOT_WIDTH, | |
"PLOT_DEPTH":PLOT_DEPTH, | |
"GAME_STATE":game_state.get_state(), | |
"KITS":kits | |
} | |
try: | |
html=open('index.html','r',encoding='utf-8').read() | |
script=f""" | |
<script> | |
window.ALL_INITIAL_OBJECTS={json.dumps(injected_state['ALL_INITIAL_OBJECTS'])}; | |
window.PLOTS_METADATA={json.dumps(injected_state['PLOTS_METADATA'])}; | |
window.SELECTED_OBJECT_TYPE={json.dumps(injected_state['SELECTED_OBJECT_TYPE'])}; | |
window.PLOT_WIDTH={json.dumps(injected_state['PLOT_WIDTH'])}; | |
window.PLOT_DEPTH={json.dumps(injected_state['PLOT_DEPTH'])}; | |
window.GAME_STATE={json.dumps(injected_state['GAME_STATE'])}; | |
window.KITS={json.dumps(injected_state['KITS'])}; | |
console.log('State injected',{{objs:window.ALL_INITIAL_OBJECTS.length,plots:window.PLOTS_METADATA.length, kits:Object.keys(window.KITS).length}}); | |
</script> | |
""" | |
html_mod=html.replace('</head>',script+'\n</head>',1) | |
components.html(html_mod,height=750,scrolling=False) | |
except FileNotFoundError: | |
st.error("index.html missing!") | |
except Exception as e: | |
st.error(f"HTML inject error: {e}") |