from __future__ import annotations import importlib.util import sys import tempfile import types import unittest from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent FIXTURE_DIR = SCRIPT_DIR / "fixtures" / "mtl-space-reference" def load_convert_module(): sys.modules.setdefault("bpy", types.ModuleType("bpy")) sys.modules.setdefault("bmesh", types.ModuleType("bmesh")) mathutils = sys.modules.setdefault("mathutils", types.ModuleType("mathutils")) mathutils.Vector = object spec = importlib.util.spec_from_file_location( "healer_man_convert_dungeon", SCRIPT_DIR / "convert-dungeon.py", ) if spec is None or spec.loader is None: raise RuntimeError("Could not load convert-dungeon.py") module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module class MtlTexturePathCorrectionTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.convert = load_convert_module() def test_preserves_the_full_map_remainder_for_the_importer(self): corrections = self.convert.mtl_texture_path_corrections(FIXTURE_DIR) self.assertEqual(corrections, [{ "mtl": "scene.mtl", "line": "3", "splitReference": "CFD_METAL", "exactReference": "CFD_METAL FLOOR.png", "splitPath": "CFD_METAL", "exactPath": "CFD_METAL FLOOR.png", }]) importer = types.SimpleNamespace() with self.convert.corrected_wowobj_texture_loader(importer, FIXTURE_DIR): with importer.open(FIXTURE_DIR / "scene.mtl", "r", encoding="utf-8") as stream: texture_lines = [ line.split()[1] for line in stream if line.strip().startswith("map_Kd") ] self.assertEqual(texture_lines, ["CFD_METAL FLOOR.png"]) self.assertFalse(hasattr(importer, "open")) def test_resolves_two_materials_with_the_same_truncated_token_by_line_context(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "scene.mtl").write_text( "newmtl library_trim_02\n" "map_Kd WD_LIBRARYTRIM 02.png\n" "newmtl library_trim_03\n" "map_Kd WD_LIBRARYTRIM 03.png\n", encoding="utf-8", ) (root / "WD_LIBRARYTRIM 02.png").write_bytes(b"02") (root / "WD_LIBRARYTRIM 03.png").write_bytes(b"03") importer = types.SimpleNamespace() materials = {} material_name = "" with self.convert.corrected_wowobj_texture_loader(importer, root) as corrections: with importer.open(root / "scene.mtl", "r", encoding="utf-8") as stream: for line in stream: fields = line.split() if not fields: continue if fields[0] == "newmtl": material_name = fields[1] elif fields[0] == "map_Kd": materials[material_name] = fields[1] self.assertEqual(materials, { "library_trim_02": "WD_LIBRARYTRIM 02.png", "library_trim_03": "WD_LIBRARYTRIM 03.png", }) self.assertEqual( [correction["exactReference"] for correction in corrections], ["WD_LIBRARYTRIM 02.png", "WD_LIBRARYTRIM 03.png"], ) def test_rejects_ambiguous_candidates_when_the_exact_reference_is_absent(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "scene.mtl").write_text( "map_Kd WD_LIBRARYTRIM missing.png\n", encoding="utf-8", ) (root / "WD_LIBRARYTRIM 02.png").write_bytes(b"02") (root / "WD_LIBRARYTRIM 03.png").write_bytes(b"03") with self.assertRaisesRegex(RuntimeError, "ambiguous whitespace-truncated texture"): self.convert.mtl_texture_path_corrections(root) class ObjNonFiniteFaceCorrectionTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.convert = load_convert_module() def test_omits_only_a_terminal_incomplete_wow_export_triangle(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) obj = root / "scene.obj" obj.write_text( "# Exported using wow.export v0.2.19\n" "g gates0\n" "f 1/1/1 2/2/2 3/3/3\n" "f 4/4/4 5/5/5 NaN/NaN/NaN\n" "g gates1\n" "f 6/6/6 7/7/7 8/8/8\n", encoding="utf-8", ) corrections = self.convert.obj_nonfinite_face_corrections(root) self.assertEqual(corrections, [{ "obj": "scene.obj", "line": "4", "group": "gates0", "sourceRecord": "f 4/4/4 5/5/5 NaN/NaN/NaN", "validVertexIndices": [4, 5], "danglingIndexCount": 2, "repair": "omit-terminal-incomplete-triangle", }]) importer = types.SimpleNamespace() with self.convert.corrected_wowobj_texture_loader( importer, root, corrections, ): with importer.open(obj, "rb") as stream: imported_lines = list(stream) imported = b"".join(imported_lines) self.assertNotIn(b"NaN", imported) self.assertIn(b"f 1/1/1 2/2/2 3/3/3", imported) self.assertIn(b"f 6/6/6 7/7/7 8/8/8", imported) def test_rejects_a_nonterminal_nonfinite_face(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "scene.obj").write_text( "# Exported using wow.export v0.2.19\n" "g gates0\n" "f 1/1/1 2/2/2 NaN/NaN/NaN\n" "f 1/1/1 2/2/2 3/3/3\n", encoding="utf-8", ) with self.assertRaisesRegex(RuntimeError, "not the terminal"): self.convert.obj_nonfinite_face_corrections(root) def test_rejects_nonfinite_vertex_data(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) (root / "scene.obj").write_text( "# Exported using wow.export v0.2.19\n" "v NaN 0 0\n", encoding="utf-8", ) with self.assertRaisesRegex(RuntimeError, "cannot be reconstructed"): self.convert.obj_nonfinite_face_corrections(root) if __name__ == "__main__": unittest.main(argv=[sys.argv[0]])