Newer
Older
SmTIAS-LightSim / src / sm_tias_lightsim / geometry / loader.py
"""STL ファイル読み込みモジュール.

trimesh を使って STL を読み込み,頂点・面・単位情報を LoadedMesh として返す.
"""

from __future__ import annotations

import pathlib
from dataclasses import dataclass
from typing import Literal

import numpy
import trimesh


@dataclass(frozen=True)
class LoadedMesh:
    """STL から読み込んだメッシュ情報.

    Attributes:
        vertices: 頂点配列 (N_v, 3), float64.座標は読み込み時の単位そのまま.
        faces: 三角形面配列 (N_f, 3), int64.各値は vertices の行インデックス.
        unit: 元 STL の単位("mm" または "m").scale_vertices で変換するときの参照.
        source_path: 元ファイルのパス(デバッグ用).
    """

    vertices: numpy.ndarray
    faces: numpy.ndarray
    unit: Literal["mm", "m"]
    source_path: pathlib.Path


def load_stl(path: str | pathlib.Path, unit: Literal["mm", "m"]) -> LoadedMesh:
    """STL ファイルを trimesh で読み込み LoadedMesh として返す.

    trimesh は STL の単位情報を持たないため,呼び出し側が `unit` を明示する必要がある.
    バイナリ・ASCII の両形式は trimesh が自動判別するため,特別な処理は不要.

    Args:
        path: STL ファイルパス.
        unit: STL の単位指定.SPEC_02 の `geometry.meshes[].unit` と一致する.

    Returns:
        頂点・面・単位を保持する LoadedMesh.

    Raises:
        FileNotFoundError: ファイルが存在しない場合.
        ValueError: STL のパースに失敗した場合,または三角形メッシュでない場合.
    """
    resolved = pathlib.Path(path).resolve()

    if not resolved.exists():
        msg = f"STL file not found: {resolved}"
        raise FileNotFoundError(msg)

    loaded = trimesh.load_mesh(str(resolved), file_type="stl")

    # trimesh.load_mesh は複数メッシュの場合 Scene を返す場合がある
    if not isinstance(loaded, trimesh.Trimesh):
        msg = (
            f"Expected a single Trimesh, got {type(loaded).__name__}. "
            "Multi-mesh STL files are not supported."
        )
        raise ValueError(msg)

    vertices = loaded.vertices.astype(numpy.float64)
    faces = loaded.faces.astype(numpy.int64)

    return LoadedMesh(
        vertices=vertices,
        faces=faces,
        unit=unit,
        source_path=resolved,
    )