Newer
Older
SmTIAS-LightSim / tests / visualize / conftest.py
"""visualize テスト共通フィクスチャ.

照度ベイクテスト・ビューアテストで使用する SceneConfig と STL ファイルを提供する.
Mitsuba をモックせず実オブジェクトを使う(testing.md モック方針).
"""

from __future__ import annotations

from pathlib import Path

import numpy as np
import pytest
import trimesh

from sm_tias_lightsim.scene.schema import SceneConfig


def _write_flat_plane_stl(path: Path, z_mm: float = 0.0) -> None:
    """z=z_mm の水平三角形 1 枚の STL を書き出す(mm 単位).

    三角形の頂点:
        v0 = (0,  0, z_mm)
        v1 = (10, 0, z_mm)
        v2 = (10, 10, z_mm)
    法線は +Z 方向(仕様に合わせ正規化は trimesh に任せる).
    """
    vertices = np.array(
        [
            [0.0, 0.0, z_mm],
            [10.0, 0.0, z_mm],
            [10.0, 10.0, z_mm],
        ],
        dtype=np.float64,
    )
    faces = np.array([[0, 1, 2]], dtype=np.int64)
    mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
    mesh.export(str(path))


def _write_blocker_stl(path: Path) -> None:
    """LED と評価板の間に配置する遮蔽板 STL を書き出す(mm 単位).

    z=50mm に大きな平面を置く(光源 z=100mm, 評価板 z=0mm の中間).
    頂点: (−100, −100, 50), (100, −100, 50), (100, 100, 50), (−100, 100, 50)
    法線は +Z 方向.
    """
    vertices = np.array(
        [
            [-100.0, -100.0, 50.0],
            [100.0, -100.0, 50.0],
            [100.0, 100.0, 50.0],
            [-100.0, 100.0, 50.0],
        ],
        dtype=np.float64,
    )
    # 2 三角形で四角形を構成
    faces = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.int64)
    mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
    mesh.export(str(path))


def _write_downward_plane_stl(path: Path, z_mm: float = 0.0) -> None:
    """法線が -Z 方向の水平三角形 1 枚の STL を書き出す(mm 単位).

    頂点の巻き順を逆にして -Z 法線を作る.
    """
    vertices = np.array(
        [
            [0.0, 0.0, z_mm],
            [10.0, 10.0, z_mm],
            [10.0, 0.0, z_mm],
        ],
        dtype=np.float64,
    )
    faces = np.array([[0, 1, 2]], dtype=np.int64)
    mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
    mesh.export(str(path))


def _make_minimal_scene_config(
    mesh_path: str,
    mesh_id: str = "plane",
    light_dict: dict | None = None,
) -> SceneConfig:
    """最小限の SceneConfig を辞書から作成する.

    Args:
        mesh_path: STL ファイルへのパス文字列(プロジェクトルート相対 or 絶対).
        mesh_id: メッシュ ID.
        light_dict: lights リストの要素辞書 1 個.None なら PointLight のデフォルトを使う.
    """
    if light_dict is None:
        light_dict = {
            "id": "led",
            "type": "point",
            "position": [0.0, 0.0, 100.0],
            "intensity": 1.0,
            "profile": "lambertian",
        }

    data = {
        "name": "test_bake",
        "geometry": {
            "meshes": [
                {
                    "id": mesh_id,
                    "path": mesh_path,
                    "unit": "mm",
                    "material": "white",
                }
            ]
        },
        "materials": {
            "white": {"type": "diffuse", "reflectance": 0.9},
        },
        "lights": [light_dict],
        "sensor": {
            "type": "perspective",
            "position": [0.0, 0.0, 200.0],
            "target": [0.0, 0.0, 0.0],
            "up": [0.0, 1.0, 0.0],
            "fov_deg": 60.0,
            "film": {"width": 64, "height": 64},
        },
        "render": {
            "variant": "scalar_rgb",
            "integrator": "path",
            "max_depth": 2,
            "spp": 1,
        },
    }
    return SceneConfig.model_validate(data)


@pytest.fixture
def flat_plane_stl(tmp_path: Path) -> Path:
    """法線 +Z の水平三角形 1 枚の STL を tmp_path に書き出す(z=0mm)."""
    path = tmp_path / "flat_plane.stl"
    _write_flat_plane_stl(path, z_mm=0.0)
    return path


@pytest.fixture
def blocker_stl(tmp_path: Path) -> Path:
    """LED (z=100mm) と評価板 (z=0mm) の間(z=50mm)に置く遮蔽板 STL."""
    path = tmp_path / "blocker.stl"
    _write_blocker_stl(path)
    return path


@pytest.fixture
def downward_plane_stl(tmp_path: Path) -> Path:
    """法線 -Z の水平三角形 1 枚の STL を tmp_path に書き出す(z=0mm)."""
    path = tmp_path / "downward_plane.stl"
    _write_downward_plane_stl(path, z_mm=0.0)
    return path


@pytest.fixture
def point_light_scene_config(flat_plane_stl: Path) -> tuple[SceneConfig, Path]:
    """PointLight + 法線 +Z 平面の SceneConfig と project_root を返す.

    光源: position=(0, 0, 100mm), intensity=1.0
    メッシュ: z=0mm 三角形(法線 +Z)
    """
    config = _make_minimal_scene_config(
        mesh_path=str(flat_plane_stl),
        mesh_id="plane",
        light_dict={
            "id": "led",
            "type": "point",
            "position": [0.0, 0.0, 100.0],
            "intensity": 1.0,
            "profile": "lambertian",
        },
    )
    return config, flat_plane_stl.parent


@pytest.fixture
def directional_light_scene_config(flat_plane_stl: Path) -> tuple[SceneConfig, Path]:
    """DirectionalLight + 法線 +Z 平面の SceneConfig と project_root を返す.

    光源: direction=(0, 0, -1)(下向き), irradiance=1.0
    メッシュ: z=0mm 三角形(法線 +Z)
    """
    config = _make_minimal_scene_config(
        mesh_path=str(flat_plane_stl),
        mesh_id="plane",
        light_dict={
            "id": "dir_light",
            "type": "directional",
            "direction": [0.0, 0.0, -1.0],
            "irradiance": 1.0,
        },
    )
    return config, flat_plane_stl.parent