diff options
Diffstat (limited to 'tests/test_scene.py')
| -rw-r--r-- | tests/test_scene.py | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/tests/test_scene.py b/tests/test_scene.py new file mode 100644 index 0000000..0e52216 --- /dev/null +++ b/tests/test_scene.py @@ -0,0 +1,125 @@ +# generat3D - evaluate Daz DSON scenes and write .dbz files +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import contextlib +import io +import json +import tempfile +import unittest +from pathlib import Path + +from generat3d import Error +from generat3d.library import Library +from generat3d.scene import load + +DATA = Path(__file__).parent / "data" + + +def load_quiet(duf, extra_roots=()): + err = io.StringIO() + with contextlib.redirect_stderr(err): + scene = load(duf, Library([DATA / "lib", *extra_roots])) + return scene, err.getvalue() + + +class SceneTest(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.dir = Path(self.tmp.name) + + def tearDown(self): + self.tmp.cleanup() + + def duf(self, scene): + p = self.dir / "s.duf" + p.write_text(json.dumps({"scene": scene})) + return p + + def base_nodes(self): + return [{"id": "Fig", "url": "/data/Test/Fig/Fig.dsf#Fig", "name": "Fig"}] + + def test_figure_and_nodes(self): + scene, _ = load_quiet(DATA / "scene.duf") + fig = scene.figure + self.assertEqual((fig.id, fig.name, fig.label), ("Fig", "Fig", "Test Figure")) + self.assertEqual([n.id for n in fig.nodes], ["Fig", "hip"]) + self.assertEqual(fig.nodes[1].rotation_order, "YZX") + self.assertEqual(fig.material_groups, ["Skin"]) + self.assertEqual(fig.verts.tolist(), [[0, 0, 0], [1, 0, 0], [0, 1, 0]]) + + def test_morphs_auto_loaded_and_broken_file_warns(self): + scene, err = load_quiet(DATA / "scene.duf") + self.assertEqual(sorted(scene.figure.morphs), ["Fig:Tall?value", "Fig:Wide?value"]) + self.assertIn("Broken.dsf", err) + + def test_dial_from_animations(self): + scene, _ = load_quiet(DATA / "scene.duf") + self.assertEqual(scene.graph.props["Fig:Tall?value"].value, 1) + + def test_dial_from_modifiers(self): + scene, _ = load_quiet(self.duf({"nodes": self.base_nodes(), "modifiers": [ + {"id": "Tall", "url": "/data/Test/Fig/Morphs/Test/Tall.dsf#Tall", + "parent": "#Fig", "channel": {"id": "value", "current_value": 0.25}}]})) + self.assertEqual(scene.graph.props["Fig:Tall?value"].value, 0.25) + + def test_bone_channels_are_props(self): + scene, _ = load_quiet(DATA / "scene.duf") + self.assertEqual(scene.graph.props["hip:hip?center_point/y"].value, 5) + self.assertEqual(scene.graph.props["Fig:Fig?end_point/y"].value, 10) + + def test_formulas_rewritten_to_keys(self): + scene, _ = load_quiet(DATA / "scene.duf") + ops = [f[1] for f in scene.graph.formulas["Fig:Wide?value"]] + self.assertEqual(ops[0][0], {"op": "push", "key": "Fig:Tall?value"}) + self.assertEqual(ops[1][0], {"op": "push", "val": 0.0}) + + def test_missing_dialed_morphs_listed_and_fatal(self): + mods = [{"id": m, "url": f"/data/Test/Fig/Morphs/Test/{m}.dsf#{m}", "parent": "#Fig", + "channel": {"id": "value", "current_value": 1}} for m in ("Gone1", "Gone2")] + with self.assertRaisesRegex(Error, "(?s)Gone1.dsf.*Gone2.dsf"): + load_quiet(self.duf({"nodes": self.base_nodes(), "modifiers": mods})) + + def test_missing_undialed_morph_is_fine(self): + mods = [{"id": "Gone", "url": "/data/Test/Fig/Morphs/Test/Gone.dsf#Gone", + "parent": "#Fig", "channel": {"id": "value", "current_value": 0}}] + load_quiet(self.duf({"nodes": self.base_nodes(), "modifiers": mods})) + + def test_missing_figure_is_fatal(self): + with self.assertRaisesRegex(Error, "Nope.dsf"): + load_quiet(self.duf({"nodes": [{"id": "X", "url": "/data/Nope.dsf#X"}]})) + + def test_no_figure_is_fatal(self): + with self.assertRaisesRegex(Error, "no figure"): + load_quiet(self.duf({"nodes": []})) + + def test_two_figures_is_fatal(self): + nodes = self.base_nodes() + [{"id": "Fig2", "url": "/data/Test/Fig/Fig.dsf#Fig"}] + with self.assertRaisesRegex(Error, "one figure"): + load_quiet(self.duf({"nodes": nodes})) + + def test_follower_skipped_with_warning(self): + nodes = self.base_nodes() + [{"id": "Shirt", "url": "/data/Test/Fig/Fig.dsf#Fig", + "name": "Shirt", "conform_target": "#Fig"}] + scene, err = load_quiet(self.duf({"nodes": nodes})) + self.assertEqual(scene.figure.id, "Fig") + self.assertIn("Shirt", err) + + def test_pose_ignored_with_warning(self): + nodes = self.base_nodes() + [{"id": "hip", "url": "/data/Test/Fig/Fig.dsf#hip", + "rotation": [{"id": "x", "current_value": 30}]}] + _, err = load_quiet(self.duf({"nodes": nodes})) + self.assertIn("pose", err) |
