# 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