OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux commits / 0fe3f60

Node graph edits drive the live viewport
Editing the graph while the viewport renders now re-forms the fractal: move a parameter, cut a link, and the attractor changes under the cursor. A depsgraph handler flags the session when its bound tree is edited, and the draw handler re-reads the graph on the next frame. Only on change - walking the tree every redraw would be wasted work and would fight the panel sliders. A graph edit can alter topology as well as values, so it marks `weights` dirty, which rebuilds the xaos alias texture too. Also fixed: Build Node Graph set `space.node_tree` without `space.tree_type`, so the editor stayed on Shader nodes and the graph looked like it had not been created. It now switches the editor. A graph whose iterator count no longer matches the document UNBINDS rather than rendering a mismatch - `apply_tree` matches positionally, so a stale graph would otherwise silently drive the wrong iterators. Verified: binding, a parameter reaching the document, removing a link removing exactly that transition ({0,2,3} -> {2,3}), the count-mismatch unbind, that the handler is installed, and that it tolerates firing with no session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0fe3f60c3dfad6ab86733ec9ab405f3f2113c692
by plankatron <fleeky@gmail.com> · 2026-08-17T01:30:20.000Z

Changed files (3) +73 -0

blender/__init__.py +2 -0
……@@ -535,12 +535,14 @@ def register():
535535 for cls in _CLASSES:
536536 bpy.utils.register_class(cls)
537537 ifs_nodes.register()
538 ifs_viewport.install_handlers()
538539 bpy.types.Scene.ifs = bpy.props.PointerProperty(type=IFSSceneSettings)
539540
540541
541542def unregister():
542543 # A live draw handler outlives the add-on and crashes on the next redraw.
543544 ifs_viewport.stop()
545 ifs_viewport.remove_handlers()
544546 ifs_nodes.unregister()
545547 del bpy.types.Scene.ifs
546548 for cls in reversed(_CLASSES):
blender/ifs_nodes.py +7 -0
……@@ -49,6 +49,7 @@ from bpy.types import Node, NodeSocket, NodeTree, Operator, Panel
4949import ifs_codegen
5050import ifs_document
5151import ifs_render
52import ifs_viewport
5253
5354# Same lazy cache as the add-on's: walking 46 plugin files on every enum draw
5455# would be absurd.
……@@ -310,9 +311,15 @@ class IFS_OT_build_graph(Operator):
310311 tree = bpy.data.node_groups.new(name, IFSNodeTree.bl_idname)
311312 build_tree(tree, doc, library())
312313
314 # tree_type as well as node_tree: assigning the tree alone leaves the
315 # editor showing Shader nodes and the graph apparently missing.
313316 for area in context.screen.areas:
314317 if area.type == 'NODE_EDITOR':
318 area.spaces.active.tree_type = IFSNodeTree.bl_idname
315319 area.spaces.active.node_tree = tree
320
321 if ifs_viewport.is_running():
322 ifs_viewport.session().bind_tree(tree.name)
316323 settings.status = f"Graph built: {len(doc.iterators)} iterators"
317324 self.report({'INFO'}, settings.status)
318325 return {'FINISHED'}
blender/ifs_viewport.py +64 -0
……@@ -166,9 +166,23 @@ class Session:
166166 self._dirty_params = False
167167 self._dirty_weights = False
168168
169 # A node graph, when one is bound, becomes the source of truth for the
170 # iterators. Re-read only when Blender says it changed - walking the tree
171 # every redraw would be wasted work and would fight the panel sliders.
172 self.tree_name = None
173 self._dirty_graph = False
174
169175 self._build_buffers()
170176 self._resize(64, 64)
171177
178 def bind_tree(self, name: str | None):
179 """Makes a node tree drive this session's iterators."""
180 self.tree_name = name
181 self._dirty_graph = name is not None
182
183 def mark_graph_dirty(self):
184 self._dirty_graph = True
185
172186 def mark_dirty(self, kind: str):
173187 """
174188 Flags what a UI edit invalidated.
……@@ -214,6 +228,26 @@ class Session:
214228 iterator.vec3_params = [tuple(item.vec3_values[i * 3:i * 3 + 3])
215229 for i in range(len(iterator.vec3_params))]
216230
231 def _apply_graph(self):
232 """Pulls the bound node graph onto the document."""
233 if not self._dirty_graph or self.tree_name is None:
234 return
235 self._dirty_graph = False
236 tree = bpy.data.node_groups.get(self.tree_name)
237 if tree is None:
238 self.tree_name = None
239 return
240 import ifs_nodes
241 try:
242 ifs_nodes.apply_tree(tree, self.doc, self.library)
243 except ValueError:
244 # Iterator count no longer matches - the graph is for another
245 # document. Unbind rather than render a mismatch.
246 self.tree_name = None
247 return
248 # A graph edit can change topology and weights, so assume the most.
249 self.mark_dirty('weights')
250
217251 def _rebuild_dirty(self):
218252 if not self._dirty_params:
219253 return
……@@ -368,6 +402,7 @@ class Session:
368402 settings = getattr(context.scene, "ifs", None)
369403 if settings is not None and settings.iterators:
370404 self.sync_from_scene(settings)
405 self._apply_graph()
371406 self._rebuild_dirty()
372407
373408 space = context.space_data
……@@ -456,6 +491,10 @@ def start(document_path, iters_per_frame=400):
456491 global _session
457492 stop()
458493 session = Session(document_path, iters_per_frame)
494 for tree in bpy.data.node_groups:
495 if tree.bl_idname == 'IFSNodeTree':
496 session.bind_tree(tree.name)
497 break
459498 session.handle = bpy.types.SpaceView3D.draw_handler_add(
460499 _draw_callback, (session,), 'WINDOW', 'POST_PIXEL')
461500 _session = session
……@@ -481,6 +520,31 @@ def stop():
481520 _session = None
482521
483522
523@bpy.app.handlers.persistent
524def _on_depsgraph_update(scene, depsgraph):
525 """Flags the session when its bound node tree is edited."""
526 if _session is None or _session.tree_name is None:
527 return
528 for update in depsgraph.updates:
529 if getattr(update.id, "name", None) == _session.tree_name:
530 _session.mark_graph_dirty()
531 for window in bpy.context.window_manager.windows:
532 for area in window.screen.areas:
533 if area.type == 'VIEW_3D':
534 area.tag_redraw()
535 return
536
537
538def install_handlers():
539 if _on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
540 bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update)
541
542
543def remove_handlers():
544 if _on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
545 bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update)
546
547
484548def is_running():
485549 return _session is not None
486550