3DModelGen / viewer.py
tomiconic's picture
Upload 9 files
af54811 verified
from __future__ import annotations
import json
from pathlib import Path
from typing import Iterable
import numpy as np
import trimesh
def _sample_points(points: np.ndarray, max_points: int = 3500) -> np.ndarray:
if len(points) <= max_points:
return points.astype(float)
idx = np.linspace(0, len(points) - 1, max_points).astype(int)
return points[idx].astype(float)
def load_points_from_cloud_file(path: str | Path, max_points: int = 3500) -> np.ndarray:
cloud = trimesh.load(path, force="mesh")
if isinstance(cloud, trimesh.points.PointCloud):
points = np.asarray(cloud.vertices)
elif isinstance(cloud, trimesh.Trimesh):
if len(cloud.faces) > 0:
count = min(max_points * 2, max(1200, len(cloud.faces) * 3))
points = cloud.sample(count)
else:
points = np.asarray(cloud.vertices)
else:
points = np.asarray(getattr(cloud, "vertices", []))
return _sample_points(np.asarray(points, dtype=float), max_points=max_points)
def load_points_from_mesh_file(path: str | Path, max_points: int = 3500) -> np.ndarray:
mesh = trimesh.load(path, force="mesh")
if isinstance(mesh, trimesh.Scene):
mesh = trimesh.util.concatenate([g for g in mesh.geometry.values() if isinstance(g, trimesh.Trimesh)])
if isinstance(mesh, trimesh.Trimesh):
if len(mesh.faces) > 0:
count = min(max_points * 2, max(1600, len(mesh.faces) * 2))
points = mesh.sample(count)
else:
points = np.asarray(mesh.vertices)
else:
points = np.asarray(getattr(mesh, "vertices", []))
return _sample_points(np.asarray(points, dtype=float), max_points=max_points)
def empty_viewer_html(message: str = "Generate a blueprint to preview it here.") -> str:
return f"""
<div style='height:520px;border-radius:20px;border:1px solid rgba(255,255,255,.08);display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,#06070a,#0b1020);color:#cfd6ff;font-family:Inter,system-ui,sans-serif;'>
<div style='text-align:center;padding:18px 24px;max-width:420px;'>
<div style='font-size:1.12rem;font-weight:700;margin-bottom:6px;'>Blueprint Viewer</div>
<div style='opacity:.84;line-height:1.45'>{message}</div>
</div>
</div>
"""
def point_cloud_viewer_html(points: np.ndarray, status: str = "Blueprint") -> str:
points = np.asarray(points, dtype=float)
if points.size == 0:
return empty_viewer_html("No points to display yet.")
points = _sample_points(points)
mins = points.min(axis=0)
maxs = points.max(axis=0)
center = (mins + maxs) / 2.0
span = float(np.max(maxs - mins)) or 1.0
normalized = (points - center) / span
color_src = normalized - normalized.min(axis=0, keepdims=True)
denom = color_src.ptp(axis=0, keepdims=True)
denom[denom == 0] = 1.0
colors = np.clip(color_src / denom, 0.0, 1.0)
points_payload = np.round(normalized, 5).tolist()
colors_payload = np.round(colors, 5).tolist()
return f"""
<div style="height:560px;border-radius:20px;overflow:hidden;border:1px solid rgba(255,255,255,.08);background:radial-gradient(circle at 50% 20%, #0f1630, #05070d 75%);position:relative;">
<div id="pb3d-label" style="position:absolute;top:12px;left:12px;z-index:2;background:rgba(16,21,40,.75);border:1px solid rgba(255,255,255,.08);backdrop-filter:blur(8px);padding:10px 12px;border-radius:14px;color:#eef2ff;font:600 14px/1.3 Inter,system-ui,sans-serif;">{status}<div style="opacity:.75;font-weight:500;margin-top:4px">One finger orbit • two fingers pan/zoom</div></div>
<canvas id="pb3d-canvas" style="width:100%;height:100%;display:block;touch-action:none"></canvas>
</div>
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
<script src="https://unpkg.com/three@0.160.0/examples/js/controls/OrbitControls.js"></script>
<script>
(() => {{
const root = document.currentScript.previousElementSibling.previousElementSibling.previousElementSibling;
const canvas = root.querySelector('canvas');
const holder = root;
if (!window.THREE || !window.THREE.OrbitControls) {{
holder.innerHTML = `<div style='height:100%;display:flex;align-items:center;justify-content:center;color:#eef2ff;font-family:Inter,system-ui,sans-serif;'>Viewer failed to load.</div>`;
return;
}}
const pts = {json.dumps(points_payload)};
const cols = {json.dumps(colors_payload)};
const THREE = window.THREE;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(50, holder.clientWidth / holder.clientHeight, 0.01, 100);
camera.position.set(1.8, 1.4, 2.2);
const renderer = new THREE.WebGLRenderer({{canvas, antialias:true, alpha:true}});
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(holder.clientWidth, holder.clientHeight, false);
renderer.outputColorSpace = THREE.SRGBColorSpace;
const controls = new THREE.OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.enablePan = true;
controls.minDistance = 0.4;
controls.maxDistance = 8;
controls.target.set(0,0,0);
const positions = new Float32Array(pts.length * 3);
const colors = new Float32Array(cols.length * 3);
for (let i = 0; i < pts.length; i++) {{
positions[i*3] = pts[i][0];
positions[i*3+1] = pts[i][2];
positions[i*3+2] = pts[i][1];
colors[i*3] = cols[i][0] * 0.85 + 0.15;
colors[i*3+1] = cols[i][1] * 0.85 + 0.15;
colors[i*3+2] = cols[i][2] * 0.85 + 0.15;
}}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({{size: 0.025, sizeAttenuation: true, vertexColors:true}});
const cloud = new THREE.Points(geometry, material);
scene.add(cloud);
const grid = new THREE.GridHelper(2.4, 12, 0x4c5cff, 0x1d2645);
grid.position.y = -0.72;
scene.add(grid);
const axes = new THREE.AxesHelper(0.7);
scene.add(axes);
const lightA = new THREE.DirectionalLight(0xffffff, 1.8);
lightA.position.set(2, 3, 2);
scene.add(lightA);
const lightB = new THREE.DirectionalLight(0x7795ff, 0.9);
lightB.position.set(-2, -1, -1.5);
scene.add(lightB);
scene.add(new THREE.AmbientLight(0xb8c8ff, 0.8));
function resize() {{
const w = holder.clientWidth;
const h = holder.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h, false);
}}
window.addEventListener('resize', resize);
function tick() {{
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(tick);
}}
tick();
}})();
</script>
"""