From 3802f311f135346487c5c1d5120e34b3aed82b13 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 28 Oct 2026 10:27:35 +0100 Subject: Add formula graph with stages, clamps and cycle detection Co-Authored-By: Claude Haiku 5.5 --- generat3d/formula.py | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 generat3d/formula.py (limited to 'generat3d') diff --git a/generat3d/formula.py b/generat3d/formula.py new file mode 100644 index 0000000..4a6a0d9 --- /dev/null +++ b/generat3d/formula.py @@ -0,0 +1,116 @@ +# 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 math +import operator + +from . import Error + +STAGES = ("sum", "mult") +BINARY = {"add": operator.add, "sub": operator.sub, + "mult": operator.mul, "div": operator.truediv} +SPLINES = () # filled in by Task 4 + + +class Prop: + __slots__ = ("value", "lo", "hi", "clamped") + + def __init__(self, value, lo, hi, clamped): + self.value, self.lo, self.hi, self.clamped = value, lo, hi, clamped + + +class Graph: + """Properties keyed by string, plus the formulas that drive them.""" + + def __init__(self): + self.props = {} + self.formulas = {} # output key -> [(stage, ops, source)] + + def add_prop(self, key, value=0.0, lo=-math.inf, hi=math.inf, clamped=False): + self.props[key] = Prop(value, lo, hi, clamped) + + def set_value(self, key, value): + self.props[key].value = value + + def add_formula(self, output, ops, stage="sum", source="?"): + if stage not in STAGES: + raise Error(f"{source}: unknown formula stage {stage!r} for {output}") + self.formulas.setdefault(output, []).append((stage, ops, source)) + + def evaluate(self): + """Final value of every property.""" + done = {} + for key in self.props: + self._value(key, done, []) + return done + + def _value(self, key, done, path): + if key in done: + return done[key] + if key in path: + raise Error("formula cycle: " + " -> ".join(path[path.index(key):] + [key])) + prop = self.props.get(key) + if prop is None: + return None + path.append(key) + total, factor = prop.value, 1.0 + for stage, ops, source in self.formulas.get(key, ()): + r = self._run(key, ops, done, path, source) + if r is None: + continue + if stage == "sum": + total += r + else: + factor *= r + path.pop() + v = total * factor + if prop.clamped: + v = min(max(v, prop.lo), prop.hi) + done[key] = v + return v + + def _run(self, key, ops, done, path, source): + stack = [] + try: + for op in ops: + name = op["op"] + if name == "push": + if "key" in op: + v = self._value(op["key"], done, path) + if v is None: + return None + stack.append(v) + else: + stack.append(op["val"]) + elif name in BINARY: + b, a = stack.pop(), stack.pop() + stack.append(BINARY[name](a, b)) + elif name == "neg": + stack.append(-stack.pop()) + elif name in SPLINES: + stack.append(_spline_op(name, stack)) + else: + raise Error(f"{source}: unknown formula operation {name!r} for {key}") + except (IndexError, KeyError, TypeError, ZeroDivisionError, ValueError): + raise Error(f"{source}: malformed formula for {key}") from None + if len(stack) != 1: + raise Error(f"{source}: malformed formula for {key}") + return stack[0] + + +def _spline_op(name, stack): + raise NotImplementedError # Task 4 -- cgit v1.2.3