From 903a963d7e1946b219c4bd376c7fa66f809f6a9b Mon Sep 17 00:00:00 2001 From: leo <80651389+civilwarinfrance@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:22:43 -0700 Subject: [PATCH] Accept optional texture vector components Preserve filenames and subsequent options after one- or two-component scale, offset and turbulence vectors. Add parser and OBJ/MTL regressions. Prepared with Codex assistance. --- pywavefront/texture.py | 32 ++++++++++++++--------- tests/test_texture.py | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/pywavefront/texture.py b/pywavefront/texture.py index ffc5caf..26edba2 100644 --- a/pywavefront/texture.py +++ b/pywavefront/texture.py @@ -34,6 +34,7 @@ # See: http://paulbourke.net/dataformats/mtl/ import pywavefront +from itertools import chain from pathlib import Path, PureWindowsPath import re @@ -145,26 +146,33 @@ def parse_o(self): """The -o option offsets the position of the texture map on the surface by shifting the position of the map origin. """ - u = float(next(self._gen)) - v = float(next(self._gen)) - w = float(next(self._gen)) - self._options.o = u, v, w + self._options.o = self._parse_vector(0.0) def parse_s(self): """The -s option scales the size of the texture pattern on the textured surface by expanding or shrinking the pattern """ - u = float(next(self._gen)) - v = float(next(self._gen)) - w = float(next(self._gen)) - self._options.s = u, v, w + self._options.s = self._parse_vector(1.0) def parse_t(self): """The -t option turns on turbulence for textures.""" - u = float(next(self._gen)) - v = float(next(self._gen)) - w = float(next(self._gen)) - self._options.t = u, v, w + self._options.t = self._parse_vector(0.0) + + def _parse_vector(self, default): + """Read required u and optional v/w, preserving the following token.""" + values = [float(next(self._gen))] + for _ in range(2): + try: + item = next(self._gen) + except StopIteration: + break + try: + values.append(float(item)) + except ValueError: + self._gen = chain([item], self._gen) + break + + return tuple(values) + (default,) * (3 - len(values)) def parse_texres(self): """The -texres option specifies the resolution of texture created when an diff --git a/tests/test_texture.py b/tests/test_texture.py index e1251fc..995ccf1 100644 --- a/tests/test_texture.py +++ b/tests/test_texture.py @@ -2,6 +2,9 @@ import os from pathlib import Path +import pytest + +import pywavefront import pywavefront.texture import utils @@ -74,3 +77,58 @@ def test_options(self): self.assertEqual(opts.s, (1.0, 1.0, 1.0)) self.assertEqual(opts.t, (0.0, 0.0, 0.0)) self.assertEqual(opts.texres, '1024') + + +@pytest.mark.parametrize('option,default', [('o', 0.0), ('s', 1.0), ('t', 0.0)]) +@pytest.mark.parametrize('values', ['-2.5', '-2.5 3e-2', '-2.5 3e-2 4']) +@pytest.mark.parametrize('following', ['', ' -clamp on']) +def test_optional_vector_components(option, default, values, following): + name = 'textures/2 diffuse.png' + texture = pywavefront.texture.Texture( + '-{} {}{} {}'.format(option, values, following, name), '') + provided = tuple(float(value) for value in values.split()) + expected = provided + (default,) * (3 - len(provided)) + assert getattr(texture.options, option) == expected + assert texture.name == name + assert texture.options.clamp == ('on' if following else 'off') + + +@pytest.mark.parametrize('option', ['o', 's', 't']) +def test_vector_requires_numeric_first_component(option): + with pytest.raises(ValueError): + pywavefront.texture.Texture('-{} invalid texture.png'.format(option), '') + + +def test_repeated_vector_restores_defaults(): + texture = pywavefront.texture.Texture( + '-s 2 3 4 -o 1 2 3 -s 5 -o 6 -t 7 8 texture.png', '') + assert texture.options.s == (5.0, 1.0, 1.0) + assert texture.options.o == (6.0, 0.0, 0.0) + assert texture.options.t == (7.0, 8.0, 0.0) + assert texture.name == 'texture.png' + + +def test_load_material_with_optional_vector_components(tmp_path): + (tmp_path / 'model.mtl').write_text( + 'newmtl surface\n' + 'map_Kd -s 2 -clamp on diffuse.png\n' + 'map_Ka -o -0.5 0.25 ambient.png\n' + 'bump -t 0.1 -bm 0.5 bump.png\n') + model = tmp_path / 'model.obj' + model.write_text( + 'mtllib model.mtl\n' + 'v 0 0 0\nv 1 0 0\nv 0 1 0\n' + 'usemtl surface\nf 1 2 3\n') + scene = pywavefront.Wavefront(model, collect_faces=True) + material = scene.materials['surface'] + + assert scene.mesh_list[0].faces == [[0, 1, 2]] + assert material.vertices == [0, 0, 0, 1, 0, 0, 0, 1, 0] + assert material.texture.name == 'diffuse.png' + assert material.texture.options.s == (2.0, 1.0, 1.0) + assert material.texture.options.clamp == 'on' + assert material.texture_ambient.name == 'ambient.png' + assert material.texture_ambient.options.o == (-0.5, 0.25, 0.0) + assert material.texture_bump.name == 'bump.png' + assert material.texture_bump.options.t == (0.1, 0.0, 0.0) + assert material.texture_bump.options.bm == 0.5