from __future__ import annotations from pathlib import Path from typing import Any import numpy as np import open3d as o3d import trimesh class IOHandler: """Centralized input validation and file loading.""" def resolve_path(self, path: str | Path) -> Path: return Path(path).expanduser() def require_file(self, path: str | Path, label: str = "file") -> Path: resolved = self.resolve_path(path) if not resolved.exists(): raise FileNotFoundError(f"{label} not found: {resolved}") if not resolved.is_file(): raise ValueError(f"{label} is not a file: {resolved}") return resolved def ensure_parent_dir(self, path: str | Path) -> Path: resolved = self.resolve_path(path) resolved.parent.mkdir(parents=True, exist_ok=True) return resolved def load_point_cloud(self, pointcloud_path: str | Path) -> o3d.geometry.PointCloud: path = self.require_file(pointcloud_path, "Point cloud") pcd = o3d.io.read_point_cloud(str(path)) if len(pcd.points) == 0: raise ValueError(f"Point cloud is empty or unreadable: {path}") return pcd def load_mesh(self, mesh_path: str | Path) -> trimesh.Trimesh: path = self.require_file(mesh_path, "Mesh") mesh = trimesh.load(str(path), force="mesh") if mesh.is_empty: raise ValueError(f"Mesh is empty or unreadable: {path}") return mesh def require_cad(self, cad_path: str | Path) -> Path: return self.require_file(cad_path, "CAD") def save_point_cloud(self, pointcloud: o3d.geometry.PointCloud, output_path: str | Path) -> Path: path = self.ensure_parent_dir(output_path) ok = o3d.io.write_point_cloud(str(path), pointcloud) if not ok: raise IOError(f"Failed to save point cloud: {path}") return path def point_cloud_to_numpy(self, pointcloud: o3d.geometry.PointCloud) -> np.ndarray: points = np.asarray(pointcloud.points, dtype=np.float64) if points.size == 0: raise ValueError("Point cloud has no points.") return points def mesh_triangles_to_numpy(self, mesh: trimesh.Trimesh) -> tuple[np.ndarray, np.ndarray]: vertices = np.asarray(mesh.vertices, dtype=np.float64) faces = np.asarray(mesh.faces, dtype=np.int64) if len(vertices) == 0 or len(faces) == 0: raise ValueError("Mesh must contain vertices and faces.") return vertices, faces def print_mapping(self, data: dict[str, Any]) -> None: for key, value in data.items(): print(f"{key}: {value}")