from __future__ import annotations import sys from dataclasses import dataclass from pathlib import Path from ..configs import resolve_sensor_asset_dir from ..unroll import ( compute_uv_distortion_metrics, load_exported_data, recommend_edge_expand, scaled_map_cache_path, ) DEFAULT_FINGER_EXPORT_DIR = "sensors/finger/assets/30fps-0703" DEFAULT_FINGER_MAP_CACHE = f"{DEFAULT_FINGER_EXPORT_DIR}/unroll_map_uv.npz" @dataclass(frozen=True) class FingerExportPaths: """Resolved Blender export bundle paths for finger UV pipelines.""" export_dir: Path mesh: Path intrinsics: Path pose: Path def as_tuple(self) -> tuple[str, str, str]: return (str(self.mesh), str(self.intrinsics), str(self.pose)) @dataclass(frozen=True) class FingerExportBundle: """Loaded Blender export data and UV map-cache settings.""" export_paths: FingerExportPaths data: dict map_cache: str edge_expand: float def _candidate_export_dirs(export_dir) -> list[Path]: raw = Path(export_dir).expanduser() if raw.is_absolute(): return [raw] here = Path(__file__).resolve() candidates = [] bundled_assets = resolve_sensor_asset_dir("finger") if bundled_assets is not None and raw.as_posix() == DEFAULT_FINGER_EXPORT_DIR: candidates.append(bundled_assets) candidates.extend([ Path.cwd() / raw, here.parents[1] / raw, # packaged wheel filesystem layout here.parents[3] / raw, # editable install: repo/assets/... ]) if getattr(sys, "frozen", False): candidates.insert(0, Path(sys.executable).resolve().parent / raw) unique: list[Path] = [] for candidate in candidates: resolved = candidate.resolve() if resolved not in unique: unique.append(resolved) return unique def resolve_export_paths(export_dir) -> FingerExportPaths: """Return Blender export paths expected inside one folder.""" checked: list[str] = [] for root in _candidate_export_dirs(export_dir): paths = FingerExportPaths( export_dir=root, mesh=root / "mesh.npz", intrinsics=root / "camera_intrinsics.npz", pose=root / "world_to_cam.npy", ) missing = [name for name, path in vars(paths).items() if name != "export_dir" and not path.is_file()] if not missing: return paths checked.append(f"{root} (missing: {', '.join(missing)})") raise FileNotFoundError( f"Missing export file(s) for finger bundle '{export_dir}'. " f"Checked: {'; '.join(checked)}" ) def load_finger_export_bundle( export_dir, *, map_cache=DEFAULT_FINGER_MAP_CACHE, source_scale=1.0, ) -> FingerExportBundle: """Resolve export paths, load bundle data, and pick the UV map cache path.""" export_paths = resolve_export_paths(export_dir) if Path(map_cache) == Path(DEFAULT_FINGER_MAP_CACHE): map_cache = str(export_paths.export_dir / "unroll_map_uv.npz") map_cache = scaled_map_cache_path(map_cache, source_scale) mesh_path, intrinsics_path, pose_path = export_paths.as_tuple() data = load_exported_data(mesh_path, intrinsics_path, pose_path) distortion = compute_uv_distortion_metrics(data) edge_expand = recommend_edge_expand(distortion) return FingerExportBundle( export_paths=export_paths, data=data, map_cache=map_cache, edge_expand=float(edge_expand), )