Spaces:
Running on Zero
Running on Zero
| 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> | |
| """ | |