From 1dc3425b7cbb02b0b052abe2f4c4ff10b09fac61 Mon Sep 17 00:00:00 2001 From: leo <80651389+civilwarinfrance@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:51:59 -0700 Subject: [PATCH] Honor material creation when loading cached models Preserve create_materials in CacheLoader and tolerate missing material libraries consistently with OBJ parsing. Keep cached geometry and available material definitions intact. AI assistance was used to investigate, implement, and test this change. --- README.md | 3 ++ pywavefront/cache.py | 3 ++ tests/test_cache_material_fallback.py | 72 +++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 tests/test_cache_material_fallback.py diff --git a/README.md b/README.md index afe217a..13fd7cc 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,9 @@ file is also generated describing the contents of the binary file. The binary file will be loaded the next time we attempt to load the obj file reducing the loading time significantly. +With ``create_materials=True``, cached loading tolerates missing material +libraries just like OBJ parsing, using default materials for their vertices. + Tests have shown loading time reduction by 10 to 100 times depending on the size and structure of the original obj file. diff --git a/pywavefront/cache.py b/pywavefront/cache.py index 6db8e69..bf03f1b 100644 --- a/pywavefront/cache.py +++ b/pywavefront/cache.py @@ -66,6 +66,7 @@ def __init__(self, file_name, wavefront, strict=False, create_materials=False, e self.path = self.file_name.parent self.encoding = encoding self.strict = strict + self.create_materials = create_materials self.dir = self.file_name.parent self.meta = None @@ -127,6 +128,8 @@ def _parse_mtllibs(self): encoding=self.encoding, strict=self.strict).materials except IOError: + if self.create_materials: + continue raise IOError("Failed to load mtl file:".format(self.path / mtllib)) for name, material in materials.items(): diff --git a/tests/test_cache_material_fallback.py b/tests/test_cache_material_fallback.py new file mode 100644 index 0000000..8524851 --- /dev/null +++ b/tests/test_cache_material_fallback.py @@ -0,0 +1,72 @@ +import tempfile +import unittest +from pathlib import Path + +from pywavefront import Wavefront + + +class CacheMaterialFallbackTest(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.obj = self.root / 'scene.obj' + self.libraries = [self.root / 'red.mtl', self.root / 'green.mtl'] + for path, name, color in [ + (self.libraries[0], 'red', '1 0 0'), + (self.libraries[1], 'green', '0 1 0'), + ]: + with path.open('w') as output: + output.write('newmtl {}\nKd {}\n'.format(name, color)) + with self.obj.open('w') as output: + output.write( + 'mtllib red.mtl\nmtllib green.mtl\n' + 'v 0 0 0\nv 1 0 0\nv 0 1 0\n' + 'usemtl red\nf 1 2 3\nusemtl green\nf 3 2 1\n' + ) + self.original = Wavefront(str(self.obj), cache=True) + self.assertFalse(self.original.parser.cache_loaded) + + def assert_cached_geometry(self, scene): + self.assertTrue(scene.parser.cache_loaded) + self.assertEqual(set(scene.materials), {'red', 'green'}) + for name, original in self.original.materials.items(): + loaded = scene.materials[name] + self.assertEqual(list(loaded.vertices), original.vertices) + self.assertEqual(loaded.vertex_format, original.vertex_format) + + def test_missing_libraries_with_material_creation(self): + for missing in ((0,), (1,), (0, 1)): + with self.subTest(missing=missing): + contents = {} + for i in missing: + with self.libraries[i].open() as source: + contents[i] = source.read() + try: + for i in missing: + self.libraries[i].unlink() + # The OBJ loader already tolerates these missing files. + plain = Wavefront(str(self.obj), create_materials=True) + scene = Wavefront(str(self.obj), cache=True, create_materials=True) + self.assert_cached_geometry(scene) + for i, name in enumerate(('red', 'green')): + self.assertEqual(scene.materials[name].is_default, i in missing) + self.assertEqual(scene.materials[name].diffuse, plain.materials[name].diffuse) + finally: + for i, text in contents.items(): + with self.libraries[i].open('w') as output: + output.write(text) + + def test_missing_library_without_material_creation_remains_error(self): + self.libraries[0].unlink() + with self.assertRaises(IOError): + Wavefront(str(self.obj), cache=True) + + def test_present_libraries_keep_their_materials(self): + for create_materials in (False, True): + with self.subTest(create_materials=create_materials): + scene = Wavefront(str(self.obj), cache=True, create_materials=create_materials) + self.assert_cached_geometry(scene) + for name, original in self.original.materials.items(): + self.assertEqual(scene.materials[name].diffuse, original.diffuse) + self.assertFalse(scene.materials[name].is_default)