3DWorldBuilder / app.py
awacke1's picture
Update app.py
0a0dfe3 verified
raw
history blame
8.01 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'
]
# ๐Ÿ—‚๏ธ 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 existing plot CSVs and parse their grid coords."""
try:
files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")]
except FileNotFoundError:
return []
parsed = []
for fn in files:
parts = fn[:-4].split('_')
try:
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:
continue
parsed.sort(key=lambda p: (p['grid_x'], p['grid_z']))
return parsed
def load_plot_objects(filename, x_offset, z_offset):
"""Read a plot CSV and shift each object's position by the plot offset."""
path = os.path.join(SAVE_DIR, filename)
try:
df = pd.read_csv(path)
except (FileNotFoundError, pd.errors.EmptyDataError):
return []
# ensure columns
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
if 'obj_id' not in df.columns:
df['obj_id'] = [str(uuid.uuid4()) for _ in df.index]
objs = []
for row in df.to_dict(orient='records'):
objs.append({
'obj_id': row['obj_id'],
'type': row['type'],
'pos_x': row['pos_x'] + x_offset,
'pos_y': row['pos_y'],
'pos_z': row['pos_z'] + z_offset,
'rot_x': row['rot_x'],
'rot_y': row['rot_y'],
'rot_z': row['rot_z'],
'rot_order': row['rot_order']
})
return objs
def save_plot_data(filename, objects_list, px, pz):
"""Save newly placed objects back to the corresponding plot CSV."""
path = os.path.join(SAVE_DIR, filename)
records = []
for o in objects_list:
pos = o.get('position', {})
rot = o.get('rotation', {})
if 'x' not in pos or 'y' not in pos or 'z' not in pos:
continue
records.append({
'obj_id': o.get('obj_id', str(uuid.uuid4())),
'type': o.get('type', 'Unknown'),
'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(records, columns=CSV_COLUMNS).to_csv(path, index=False)
st.success(f"๐ŸŽ‰ Saved {len(records)} objects 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('js_save_processor', None)
# ๐Ÿ”„ Load all saved plots + 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 st.sidebar:
st.title("๐Ÿ—๏ธ World Controls")
st.header("๐Ÿ“ Navigate Plots")
cols = st.columns(2)
for idx, p in enumerate(plots_metadata):
label = f"โžก๏ธ {p['name']} ({p['grid_x']},{p['grid_z']})"
if cols[idx % 2].button(label, key=f"nav_{p['id']}"):
from streamlit_js_eval import streamlit_js_eval
js = f"teleportPlayer({p['x_offset']+PLOT_WIDTH/2},{p['z_offset']+PLOT_DEPTH/2});"
try:
streamlit_js_eval(js_code=js, key=f"tp_{p['id']}")
except Exception as e:
st.error(f"Teleport failed: {e}")
st.markdown("---")
st.header("๐ŸŒฒ Place Objects")
# โ†โ”€โ”€โ”€ four new builder-tool options added here โ”€โ”€โ”€โ”€โ†’
options = [
"None",
"Simple House",
"Tree",
"Rock",
"Fence Post",
"Cyberpunk City Builder Kit",
"POLYGON - Fantasy Kingdom Pack",
"HEROIC FANTASY CREATURES FULL PACKย VOLย 1",
"POLYGON - Apocalypse Pack"
]
sel = st.selectbox("Select:", options,
index=options.index(st.session_state.selected_object)
if st.session_state.selected_object in options else 0,
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.experimental_rerun()
# ๐Ÿ“จ Handle incoming save-data callback
raw = st.session_state.get("js_save_processor")
if raw:
try:
payload = json.loads(raw) if isinstance(raw, str) else raw
pos = payload.get('playerPosition')
objs = payload.get('objectsToSave', [])
gx = math.floor(pos['x'] / PLOT_WIDTH)
gz = 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()
st.session_state.js_save_processor = None
st.experimental_rerun()
except Exception as e:
st.error(f"Error saving data: {e}")
# ๐Ÿ  Main view & HTML injection
st.header("๐ŸŒ Infinite Shared 3D World")
st.caption("โžก๏ธ Explore, click to build, ๐Ÿ’พ to save!")
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()
}
try:
with open('index.html', 'r', encoding='utf-8') as f:
html = f.read()
inject = 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.PLOT_WIDTH = {json.dumps(state["PLOT_WIDTH"])};
window.PLOT_DEPTH = {json.dumps(state["PLOT_DEPTH"])};
window.GAME_STATE = {json.dumps(state["GAME_STATE"])};
</script>
"""
html = html.replace("</head>", inject + "\n</head>", 1)
components.html(html, height=750, scrolling=False)
except FileNotFoundError:
st.error("โŒ index.html not found!")
except Exception as e:
st.error(f"HTML injection failed: {e}")