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"""
Blueprint Viewer
{message}
""" 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"""
{status}
One finger orbit • two fingers pan/zoom
"""