import os
import shutil
import html
import logging
import numpy as np
import gradio as gr
# Setup professional logging
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s - %(message)s")
logger = logging.getLogger("PaGeR-Showcase")
# Setup static folders
assets_dir = "assets"
os.makedirs(assets_dir, exist_ok=True)
# Create fallback simulation room point cloud if none exists
def generate_fallback_ply(filepath):
"""
Generates a beautiful 3D test room shell (walls, floor, ceiling)
with a red sphere and green sphere. Approximately 110,000 colored points.
"""
logger.info(f"Generating fallback point cloud at: {filepath}")
points = []
num_points_wall = 15000
# Floor (y = -2.5)
for _ in range(num_points_wall):
x = np.random.uniform(-5, 5)
z = np.random.uniform(-5, 5)
y = -2.5
r = int((x + 5) * 25.5)
g = 45
b = int((z + 5) * 25.5)
points.append(f"{x} {y} {z} {r} {g} {b}")
# Ceiling (y = 2.5)
for _ in range(num_points_wall):
x = np.random.uniform(-5, 5)
z = np.random.uniform(-5, 5)
y = 2.5
r = 45
g = int((x + 5) * 25.5)
b = int((z + 5) * 25.5)
points.append(f"{x} {y} {z} {r} {g} {b}")
# 4 Walls (x = -5, x = 5, z = -5, z = 5)
for _ in range(num_points_wall * 4):
wall = np.random.choice([0, 1, 2, 3])
y = np.random.uniform(-2.5, 2.5)
if wall == 0: # back wall z = -5
x = np.random.uniform(-5, 5)
z = -5
r = 100
g = int((y + 2.5) * 51)
b = int((x + 5) * 25.5)
elif wall == 1: # front wall z = 5
x = np.random.uniform(-5, 5)
z = 5
r = int((x + 5) * 25.5)
g = 100
b = int((y + 2.5) * 51)
elif wall == 2: # left wall x = -5
x = -5
z = np.random.uniform(-5, 5)
r = int((z + 5) * 25.5)
g = int((y + 2.5) * 51)
b = 100
else: # right wall x = 5
x = 5
z = np.random.uniform(-5, 5)
r = int((y + 2.5) * 51)
g = int((z + 5) * 25.5)
b = 150
points.append(f"{x} {y} {z} {r} {g} {b}")
# Decorative Interior Sphere 1 (Red)
for _ in range(20000):
u = np.random.random()
v = np.random.random()
theta = u * 2.0 * np.pi
phi = np.arccos(2.0 * v - 1.0)
r = 1.0
x = 1.8 + r * np.sin(phi) * np.cos(theta)
y = -1.2 + r * np.sin(phi) * np.sin(theta)
z = 1.8 + r * np.cos(phi)
points.append(f"{x} {y} {z} 239 68 68")
# Decorative Interior Sphere 2 (Green)
for _ in range(20000):
u = np.random.random()
v = np.random.random()
theta = u * 2.0 * np.pi
phi = np.arccos(2.0 * v - 1.0)
r = 1.2
x = -2.2 + r * np.sin(phi) * np.cos(theta)
y = -0.8 + r * np.sin(phi) * np.sin(theta)
z = -2.2 + r * np.cos(phi)
points.append(f"{x} {y} {z} 34 197 94")
# Construct ASCII PLY format header
header = f"""ply
format ascii 1.0
element vertex {len(points)}
property float x
property float y
property float z
property uchar red
property uchar green
property uchar blue
end_header
"""
with open(filepath, "w") as f:
f.write(header + "\n".join(points))
logger.info(f"Fallback point cloud generated successfully with {len(points)} points.")
# Pre-generate preview1 if missing
preview_ply = os.path.join(assets_dir, "preview1.ply")
if not os.path.exists(preview_ply):
generate_fallback_ply(preview_ply)
def get_ply_files():
"""Scans the assets directory and returns a sorted list of PLY point cloud files."""
files = [f for f in os.listdir(assets_dir) if f.endswith(".ply")]
logger.info(f"Scanning 'assets/' folder. Found PLY files: {files}")
if not files:
return []
return sorted([os.path.join(assets_dir, f) for f in files])
def get_viewer_html(ply_url):
"""Generates the embedded HTML template for the ThreeJS WebGL view."""
return f"""
3D Point Cloud Viewer
Loading 3D Point Cloud...
0%
Navigation
Drag Mouse Look Around
W, A, S, D Fly / Move Ground
SPACE / E Move UP
SHIFT / Q Move DOWN
"""
def update_viewer_iframe(selected_file):
"""
Constructs a responsive iframe utilizing a secure inline HTML source.
Also handles route mappings based on the Gradio major version.
"""
if not selected_file:
logger.warning("No file selected in update_viewer_iframe!")
return "No file selected."
# Map to the absolute path on the host, correcting cross-platform path separators
abs_ply_path = os.path.abspath(selected_file).replace("\\", "/")
# Check the Gradio version to decide correct file route mapping
gradio_major_version = int(gr.__version__.split(".")[0])
if gradio_major_version >= 5:
# Gradio 5 and 6 require /gradio_api/ prefix for custom assets
ply_url = f"/gradio_api/file={abs_ply_path}"
else:
# Legacy fallback
ply_url = f"/file={abs_ply_path}"
logger.info(f"Target file: '{selected_file}'")
logger.info(f"Resolved path: '{abs_ply_path}'")
logger.info(f"Gradio Major Version: {gradio_major_version}. Generated URL mapping: '{ply_url}'")
raw_html = get_viewer_html(ply_url)
escaped_html = html.escape(raw_html)
return f''
def handle_file_upload(file_obj):
"""Saves user-uploaded .ply files to assets/ and selects them."""
if file_obj is None:
return gr.update()
dest_filename = os.path.basename(file_obj.name)
dest_path = os.path.join(assets_dir, dest_filename)
logger.info(f"User uploaded file. Storing to: {dest_path}")
shutil.copy(file_obj.name, dest_path)
updated_files = get_ply_files()
return gr.update(choices=updated_files, value=dest_path)
def refresh_file_list():
"""Manual scan update for files placed into assets/ via git or CLI."""
logger.info("Manual assets/ directory scan triggered.")
updated_files = get_ply_files()
default_val = updated_files[0] if updated_files else None
return gr.update(choices=updated_files, value=default_val)
# Handle theme configuration based on Gradio version
theme = gr.themes.Default(
primary_hue="blue",
neutral_hue="slate",
).set(
body_background_fill="*neutral_950",
block_background_fill="*neutral_900",
block_border_color="*neutral_800",
button_secondary_background_fill="*neutral_800"
)
gradio_major_version = int(gr.__version__.split(".")[0])
logger.info(f"Starting application layout. Detected Gradio Major: {gradio_major_version}")
blocks_kwargs = {}
launch_kwargs = {
"allowed_paths": [os.path.abspath(assets_dir)]
}
# Resolve the Gradio 6 theme warning by adjusting kwarg targets
if gradio_major_version >= 6:
launch_kwargs["theme"] = theme
else:
blocks_kwargs["theme"] = theme
with gr.Blocks(title="PaGeR Point Cloud Showcase", **blocks_kwargs) as demo:
gr.Markdown(
"""
# 🪐 Panoramic Point Cloud Viewer
Load and navigate large panoramic point cloud files (up to **1,000,000+ points**). The camera places you directly in the **middle POV** on load, rendering optimal spatial depth.
"""
)
with gr.Row():
with gr.Column(scale=3):
file_dropdown = gr.Dropdown(
choices=get_ply_files(),
value=get_ply_files()[0] if get_ply_files() else None,
label="📁 Select Point Cloud File",
interactive=True,
)
with gr.Column(scale=1):
refresh_btn = gr.Button("🔄 Scan assets/", size="sm")
with gr.Column(scale=2):
upload_zone = gr.File(
label="📤 Drag & Drop New .PLY File Here",
file_types=[".ply"],
type="filepath",
height=80
)
# Core interactive WebGL viewport component
viewer_component = gr.HTML(
value=update_viewer_iframe(file_dropdown.value) if file_dropdown.value else ""
)
# Event routing
file_dropdown.change(
fn=update_viewer_iframe,
inputs=file_dropdown,
outputs=viewer_component
)
upload_zone.upload(
fn=handle_file_upload,
inputs=upload_zone,
outputs=file_dropdown
)
refresh_btn.click(
fn=refresh_file_list,
inputs=[],
outputs=file_dropdown
)
# Static asset paths for custom resource streaming
gr.set_static_paths(paths=[assets_dir])
if __name__ == "__main__":
logger.info(f"Launching Gradio with arguments: {launch_kwargs}")
demo.launch(**launch_kwargs)