OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux commits / eedf6ea

The xaos matrix owns the graph, and the panel stops overwriting it
Two bugs from using the node editor for real. **"the nodes dont affect the fractal at all."** sync_from_scene copied the panel's iterator values onto the document on EVERY frame, while _apply_graph only ran when the graph was flagged dirty. So a graph edit landed for exactly one frame and the panel reverted it on the next - indistinguishable from the graph being ignored. When a tree is bound it now OWNS the iterators and the panel stops writing them. Brightness and shading still sync, because those are display settings and not the graph's business. **"there was a cyclic link."** An IFS graph is inherently cyclic - iterator A follows B follows A, and a fractal flame depends on it - while Blender node trees assume a DAG and mark cycles invalid. Links were the wrong primitive to store xaos in, and they could not have held the weights anyway: NodeLink exposes only is_valid and is_muted, and a multi-input socket shares one default_value across every link into it. So the weights now live in an editable matrix on each node and the fractal reads THAT. The links stay, purely to show the structure the way IFSRenderer's own NodeMap does, and the panel says plainly that the cyclic warning is expected because nothing is read from them. The matrix is a real grid now rather than a read-only 1.0/- display: row is the source, column the destination, every cell editable, capped at 16 iterators for the UI while larger documents still render. Verified: the matrix loads the document's xaos exactly ([1,0,1,1,0,0,0,0] for the gasket's first row), a graph edit survives the next frame's panel sync, zeroing a cell removes that transition, setting one adds it with its weight, and brightness still syncs while a graph is bound. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
eedf6ea2876412c258562f54c49e7811a86396e0
by plankatron <fleeky@gmail.com> · 2026-08-17T01:46:02.000Z

Changed files (2) +51 -29

blender/ifs_nodes.py +43 -29
……@@ -13,17 +13,22 @@ evaluation is off the table, GN buys nothing and costs a shared Add menu full of
1313mesh nodes. A custom tree gets its own editor, its own Add menu, and the same
1414node-group and driver support.
1515
16## Why links carry topology and a matrix carries the weights
16## Why the MATRIX owns xaos and the links are only a picture
1717
18A 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
20share ONE `default_value` across every link into them, so a per-(source,
21destination) weight has nowhere to live on the link either.
18An IFS graph is inherently CYCLIC — iterator A follows B follows A, and a fractal
19flame depends on it. Blender node trees assume a DAG: a cycle is marked invalid
20and the editor warns about it. Links are therefore the wrong primitive to store
21xaos in, whatever else they are good for.
2222
23So links say WHICH transitions exist — the sparse structure, which is what the
24graph is good at showing — and the xaos weights live in a matrix panel, which is
25what a dense N x N of numbers is good at showing. Ten iterators is a hundred
26links; drawing those as noodles would be unreadable.
23A 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
25shares ONE `default_value` across every link into it, so a per-(source,
26destination) weight has nowhere to live.
27
28So the weights live in an editable matrix on each node, and the links exist purely
29to show the structure at a glance, the way IFSRenderer's own NodeMap does. Blender
30will still flag the cycles; that warning is expected and harmless, because nothing
31is read from the links.
2732
2833## Why this is worth having beyond looks
2934
……@@ -67,6 +72,9 @@ def library():
6772
6873MAX_REAL = 8
6974MAX_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.
77MAX_ITERATORS = 16
7078
7179
7280class IFSNodeTree(NodeTree):
……@@ -119,6 +127,11 @@ class IFSIteratorNode(Node):
119127 description="Share of RESET points that land on this iterator. Not the "
120128 "same as Weight, which scales its outgoing transitions")
121129
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)
122135 real_values: FloatVectorProperty(size=MAX_REAL)
123136 vec3_values: FloatVectorProperty(size=MAX_VEC3 * 3)
124137 real_names: StringProperty(default="")
……@@ -194,6 +207,8 @@ def build_tree(tree, doc, library):
194207 for i, vec in enumerate(iterator.vec3_params[:MAX_VEC3]):
195208 for c in range(3):
196209 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)
197212 created.append(node)
198213
199214 for source_index, iterator in enumerate(doc.iterators):
……@@ -235,19 +250,12 @@ def apply_tree(tree, doc, library):
235250 iterator.vec3_params = [tuple(node.vec3_values[i * 3:i * 3 + 3])
236251 for i in range(len(iterator.vec3_params))]
237252
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.
240255 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}
251259 return nodes
252260
253261
……@@ -276,15 +284,21 @@ class IFS_PT_xaos(Panel):
276284 layout.label(text="No iterators in this graph")
277285 return
278286
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]):
281298 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="")
288302
289303
290304class IFS_OT_build_graph(Operator):
blender/ifs_viewport.py +8 -0
……@@ -218,6 +218,14 @@ class Session:
218218 self.shading_mode = self.SHADING_MODES.get(settings.shading_mode, 0)
219219 self.opacity_gain = settings.opacity_gain
220220 self.relief = settings.relief
221
222 # When a node graph is bound it OWNS the iterators. Copying the panel's
223 # values on every frame would revert each graph edit one frame after it
224 # landed, which looks exactly like the graph doing nothing at all. The
225 # display settings above still apply; they are not the graph's business.
226 if self.tree_name is not None:
227 return
228
221229 for item, iterator in zip(settings.iterators, self.doc.iterators):
222230 iterator.opacity = item.opacity
223231 iterator.color_index = item.color_index