# generat3D - evaluate Daz DSON scenes and write .dbz files # Copyright (C) 2026 Danilo M. # # 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 unittest from generat3d import Error from generat3d.formula import Graph def push(key): return {"op": "push", "key": key} def val(v): return {"op": "push", "val": v} class GraphTest(unittest.TestCase): def setUp(self): self.g = Graph() self.g.add_prop("a", 0.5) self.g.add_prop("b") self.g.add_prop("c", 0.0, 0.0, 1.0, clamped=True) def test_base_values(self): self.assertEqual(self.g.evaluate(), {"a": 0.5, "b": 0.0, "c": 0.0}) def test_sum_stage(self): self.g.add_formula("b", [push("a"), val(2), {"op": "mult"}]) self.g.add_formula("b", [push("a"), val(1), {"op": "add"}]) self.assertEqual(self.g.evaluate()["b"], 1.0 + 1.5) def test_mult_stage(self): self.g.set_value("b", 3.0) self.g.add_formula("b", [push("a")], stage="mult") self.assertEqual(self.g.evaluate()["b"], 1.5) def test_chain_and_clamp(self): self.g.add_formula("b", [push("a"), val(4), {"op": "mult"}]) self.g.add_formula("c", [push("b")]) out = self.g.evaluate() self.assertEqual(out["b"], 2.0) self.assertEqual(out["c"], 1.0) def test_sub_div_neg(self): self.g.add_formula("b", [val(1), push("a"), {"op": "sub"}, val(4), {"op": "div"}, {"op": "neg"}]) self.assertEqual(self.g.evaluate()["b"], -0.125) def test_unknown_input_drops_formula(self): self.g.add_formula("b", [push("missing")]) self.g.add_formula("b", [val(7)]) self.assertEqual(self.g.evaluate()["b"], 7) def test_output_without_prop_is_ignored(self): self.g.add_formula("nowhere", [val(1)]) self.assertNotIn("nowhere", self.g.evaluate()) def test_cycle_is_fatal(self): self.g.add_formula("a", [push("b")]) self.g.add_formula("b", [push("a")]) with self.assertRaisesRegex(Error, "formula cycle: a -> b -> a"): self.g.evaluate() def test_unknown_op_is_fatal(self): self.g.add_formula("b", [val(1), {"op": "pow"}], source="/data/x.dsf#m") with self.assertRaisesRegex(Error, r"/data/x.dsf#m: unknown formula operation 'pow'"): self.g.evaluate() def test_unknown_stage_is_fatal(self): with self.assertRaisesRegex(Error, "unknown formula stage 'avg'"): self.g.add_formula("b", [val(1)], stage="avg") def test_stack_errors_are_fatal(self): self.g.add_formula("b", [{"op": "add"}], source="f") with self.assertRaisesRegex(Error, "f: malformed formula for b"): self.g.evaluate() def test_leftover_stack_is_fatal(self): self.g.add_formula("b", [val(1), val(2)], source="f") with self.assertRaisesRegex(Error, "f: malformed formula for b"): self.g.evaluate() def test_non_numeric_result_is_fatal(self): self.g.add_formula("b", [val("x")], source="f") with self.assertRaisesRegex(Error, "f: malformed formula for b"): self.g.evaluate() def test_overflow_is_fatal(self): self.g.add_formula("b", [val(10**400), val(0.5), {"op": "mult"}], source="f") with self.assertRaisesRegex(Error, "f: malformed formula for b"): self.g.evaluate() def test_nan_result_is_fatal(self): self.g.add_formula("b", [val(float("nan"))], source="f") with self.assertRaisesRegex(Error, "f: malformed formula for b"): self.g.evaluate() def test_deep_chain_is_fatal(self): g = Graph() n = 2000 for i in range(n): g.add_prop(f"p{i}") for i in range(n - 1): g.add_formula(f"p{i}", [push(f"p{i + 1}")]) with self.assertRaisesRegex(Error, "too deep"): g.evaluate() def test_diamond_dependency(self): g = Graph() g.add_prop("a", 1.0) g.add_prop("b") g.add_prop("c") g.add_prop("d") g.add_formula("b", [push("a")]) g.add_formula("c", [push("a"), push("a"), {"op": "add"}]) g.add_formula("d", [push("b"), push("c"), {"op": "add"}]) self.assertEqual(g.evaluate()["d"], 3) from generat3d.formula import spline def spline_ops(kind, knots): return ([{"op": "push", "key": "a"}] + [val(k) for k in knots] + [val(len(knots)), {"op": kind}]) class SplineTest(unittest.TestCase): LINE = [[0, 0], [1, 1], [2, 2], [3, 3]] def test_outside_range_holds_ends(self): for kind in ("spline_constant", "spline_linear", "spline_tcb"): self.assertEqual(spline(kind, -1, self.LINE), 0) self.assertEqual(spline(kind, 9, self.LINE), 3) def test_constant(self): self.assertEqual(spline("spline_constant", 1.7, [[0, 5], [1, 6], [2, 7]]), 6) def test_linear(self): self.assertAlmostEqual(spline("spline_linear", 0.25, [[0, 0], [1, 4]]), 1.0) def test_tcb_hits_knots(self): knots = [[0, 0, 0, 0, 0], [1, 3, 0, 0, 0], [2, 1, 0, 0, 0]] self.assertEqual(spline("spline_tcb", 1, knots), 3) def test_tcb_zero_params_is_catmull_rom(self): knots = [k + [0, 0, 0] for k in self.LINE] self.assertAlmostEqual(spline("spline_tcb", 1.5, knots), 1.5) def test_spline_in_graph(self): g = Graph() g.add_prop("a", 0.5) g.add_prop("b") g.add_formula("b", spline_ops("spline_linear", [[0, 0], [1, 10]])) self.assertAlmostEqual(g.evaluate()["b"], 5.0) def test_fractional_count_is_fatal(self): g = Graph() g.add_prop("a", 0.5) g.add_prop("b") ops = [push("a"), val([0, 0]), val([1, 1]), val(2.5), {"op": "spline_linear"}] g.add_formula("b", ops, source="f") with self.assertRaisesRegex(Error, "malformed formula for b"): g.evaluate() def test_unsorted_knots_are_fatal(self): g = Graph() g.add_prop("a", 0.5) g.add_prop("b") g.add_formula("b", spline_ops("spline_linear", [[1, 1], [0, 0], [2, 5]]), source="f") with self.assertRaisesRegex(Error, "malformed formula for b"): g.evaluate() def test_count_larger_than_stack_is_fatal(self): g = Graph() g.add_prop("a", 0.5) g.add_prop("b") ops = [push("a"), val(0), val(1), {"op": "spline_linear"}] g.add_formula("b", ops, source="f") with self.assertRaisesRegex(Error, "malformed formula for b"): g.evaluate()