aboutsummaryrefslogtreecommitdiffstats
path: root/generat3d
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-28 10:27:35 +0100
committerDanilo M. <danix@danix.xyz>2026-10-28 10:27:35 +0100
commit3802f311f135346487c5c1d5120e34b3aed82b13 (patch)
treec681d1ed7d85e469b09dd27137b52559b3b79eaf /generat3d
parentfc51e791dfbc502900d4dbf93928e5ff1633a33f (diff)
downloadgenerat3D-3802f311f135346487c5c1d5120e34b3aed82b13.tar.gz
generat3D-3802f311f135346487c5c1d5120e34b3aed82b13.zip
Add formula graph with stages, clamps and cycle detection
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Diffstat (limited to 'generat3d')
-rw-r--r--generat3d/formula.py116
1 files changed, 116 insertions, 0 deletions
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. <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 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