| … | … | @@ -13,17 +13,22 @@ evaluation is off the table, GN buys nothing and costs a shared Add menu full of |
| 13 | 13 | mesh nodes. A custom tree gets its own editor, its own Add menu, and the same |
| 14 | 14 | node-group and driver support. |
| 15 | 15 | |
| 16 | | ## Why links carry topology and a matrix carries the weights |
| 16 | ## Why the MATRIX owns xaos and the links are only a picture |
| 17 | 17 | |
| 18 | | A Blender `NodeLink` cannot hold a value: its only writable properties are |
| 19 | | `is_valid` and `is_muted`, and it rejects custom properties. Multi-input sockets |
| 20 | | share ONE `default_value` across every link into them, so a per-(source, |
| 21 | | destination) weight has nowhere to live on the link either. |
| 18 | An IFS graph is inherently CYCLIC — iterator A follows B follows A, and a fractal |
| 19 | flame depends on it. Blender node trees assume a DAG: a cycle is marked invalid |
| 20 | and the editor warns about it. Links are therefore the wrong primitive to store |
| 21 | xaos in, whatever else they are good for. |
| 22 | 22 | |
| 23 | | So links say WHICH transitions exist — the sparse structure, which is what the |
| 24 | | graph is good at showing — and the xaos weights live in a matrix panel, which is |
| 25 | | what a dense N x N of numbers is good at showing. Ten iterators is a hundred |
| 26 | | links; drawing those as noodles would be unreadable. |
| 23 | A link could not hold the weight anyway. `NodeLink` exposes only `is_valid` and |
| 24 | `is_muted` as writable and rejects custom properties, and a multi-input socket |
| 25 | shares ONE `default_value` across every link into it, so a per-(source, |
| 26 | destination) weight has nowhere to live. |
| 27 | |
| 28 | So the weights live in an editable matrix on each node, and the links exist purely |
| 29 | to show the structure at a glance, the way IFSRenderer's own NodeMap does. Blender |
| 30 | will still flag the cycles; that warning is expected and harmless, because nothing |
| 31 | is read from the links. |
| 27 | 32 | |
| 28 | 33 | ## Why this is worth having beyond looks |
| 29 | 34 | |
| … | … | @@ -67,6 +72,9 @@ def library(): |
| 67 | 72 | |
| 68 | 73 | MAX_REAL = 8 |
| 69 | 74 | MAX_VEC3 = 3 |
| 75 | # Upper bound for the xaos matrix in the UI. A document with more iterators than |
| 76 | # this still renders; only the matrix stops showing the extra columns. |
| 77 | MAX_ITERATORS = 16 |
| 70 | 78 | |
| 71 | 79 | |
| 72 | 80 | class IFSNodeTree(NodeTree): |
| … | … | @@ -119,6 +127,11 @@ class IFSIteratorNode(Node): |
| 119 | 127 | description="Share of RESET points that land on this iterator. Not the " |
| 120 | 128 | "same as Weight, which scales its outgoing transitions") |
| 121 | 129 | |
| 130 | # Outgoing xaos weights, indexed by DESTINATION iterator. This - not the |
| 131 | # links - is what the fractal reads, because an IFS graph is cyclic and |
| 132 | # Blender invalidates cyclic links. |
| 133 | xaos: FloatVectorProperty(size=MAX_ITERATORS, default=[1.0] * MAX_ITERATORS, |
| 134 | min=0.0, soft_max=4.0) |
| 122 | 135 | real_values: FloatVectorProperty(size=MAX_REAL) |
| 123 | 136 | vec3_values: FloatVectorProperty(size=MAX_VEC3 * 3) |
| 124 | 137 | real_names: StringProperty(default="") |
| … | … | @@ -194,6 +207,8 @@ def build_tree(tree, doc, library): |
| 194 | 207 | for i, vec in enumerate(iterator.vec3_params[:MAX_VEC3]): |
| 195 | 208 | for c in range(3): |
| 196 | 209 | node.vec3_values[i * 3 + c] = vec[c] |
| 210 | for dest in range(MAX_ITERATORS): |
| 211 | node.xaos[dest] = iterator.weight_to.get(dest, 0.0) |
| 197 | 212 | created.append(node) |
| 198 | 213 | |
| 199 | 214 | for source_index, iterator in enumerate(doc.iterators): |
| … | … | @@ -235,19 +250,12 @@ def apply_tree(tree, doc, library): |
| 235 | 250 | iterator.vec3_params = [tuple(node.vec3_values[i * 3:i * 3 + 3]) |
| 236 | 251 | for i in range(len(iterator.vec3_params))] |
| 237 | 252 | |
| 238 | | # Links are topology only; a missing link is a zero, an existing one keeps |
| 239 | | # whatever weight the matrix holds, defaulting to 1. |
| 253 | # xaos comes from the MATRIX, never the links: the graph is cyclic, so |
| 254 | # Blender marks those links invalid and they cannot be trusted as data. |
| 240 | 255 | for node, iterator in zip(nodes, doc.iterators): |
| 241 | | linked = {} |
| 242 | | for link in tree.links: |
| 243 | | # Compared by NAME, not identity. Blender rebuilds RNA wrappers on |
| 244 | | # every access, so `link.from_node is node` is False even for the same |
| 245 | | # node - which silently returned an empty xaos and a fractal with no |
| 246 | | # transitions at all. |
| 247 | | if link.from_node.name == node.name and link.to_node.name in index_of: |
| 248 | | dest = index_of[link.to_node.name] |
| 249 | | linked[dest] = iterator.weight_to.get(dest, 1.0) or 1.0 |
| 250 | | iterator.weight_to = linked |
| 256 | iterator.weight_to = {dest: float(node.xaos[dest]) |
| 257 | for dest in range(min(len(nodes), MAX_ITERATORS)) |
| 258 | if node.xaos[dest] != 0.0} |
| 251 | 259 | return nodes |
| 252 | 260 | |
| 253 | 261 | |
| … | … | @@ -276,15 +284,21 @@ class IFS_PT_xaos(Panel): |
| 276 | 284 | layout.label(text="No iterators in this graph") |
| 277 | 285 | return |
| 278 | 286 | |
| 279 | | layout.label(text="Rows are the source iterator", icon='INFO') |
| 280 | | for node in nodes: |
| 287 | layout.label(text="Row = source, column = destination", icon='INFO') |
| 288 | layout.label(text="Cyclic-link warnings are expected: an IFS graph is " |
| 289 | "cyclic and nothing is read from the links.") |
| 290 | |
| 291 | count = min(len(nodes), MAX_ITERATORS) |
| 292 | header = layout.row(align=True) |
| 293 | header.label(text="") |
| 294 | for index in range(count): |
| 295 | header.label(text=str(index)) |
| 296 | |
| 297 | for source, node in enumerate(nodes[:count]): |
| 281 | 298 | row = layout.row(align=True) |
| 282 | | row.label(text=node.label or node.name) |
| 283 | | outgoing = {l.to_node.name for l in tree.links if l.from_node is node} |
| 284 | | for other in nodes: |
| 285 | | sub = row.row(align=True) |
| 286 | | sub.active = other.name in outgoing |
| 287 | | sub.label(text="1.0" if other.name in outgoing else "-") |
| 299 | row.label(text=f"{source}: {node.label or node.name}") |
| 300 | for dest in range(count): |
| 301 | row.prop(node, "xaos", index=dest, text="") |
| 288 | 302 | |
| 289 | 303 | |
| 290 | 304 | class IFS_OT_build_graph(Operator): |