ref
This commit is contained in:
parent
8e8a0a5993
commit
81d93dd124
7 changed files with 753 additions and 94 deletions
|
|
@ -36,6 +36,11 @@ def register():
|
||||||
print('Nodeslicer Loaded')
|
print('Nodeslicer Loaded')
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
try:
|
||||||
|
tree.cancel_timer()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error canceling timer: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if tree.depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
|
if tree.depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
|
||||||
bpy.app.handlers.depsgraph_update_post.remove(tree.depsgraph_update_handler)
|
bpy.app.handlers.depsgraph_update_post.remove(tree.depsgraph_update_handler)
|
||||||
|
|
|
||||||
|
|
@ -3,35 +3,53 @@ from typing import Any, Callable
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty, EnumProperty, BoolProperty, FloatVectorProperty, CollectionProperty
|
from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty, EnumProperty, BoolProperty, FloatVectorProperty, CollectionProperty
|
||||||
|
|
||||||
|
def _is_btype(val: Any, type_name: str) -> bool:
|
||||||
|
if val is None:
|
||||||
|
return False
|
||||||
|
btype = getattr(bpy.types, type_name, None)
|
||||||
|
if isinstance(btype, type):
|
||||||
|
try:
|
||||||
|
if isinstance(val, btype):
|
||||||
|
return True
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
val_cls_name = getattr(val, "__class__", type(None)).__name__
|
||||||
|
return val_cls_name == type_name or f"Dummy{type_name}" in val_cls_name
|
||||||
|
|
||||||
def notify_tree_changed(self, context):
|
def notify_tree_changed(self, context):
|
||||||
from . import tree
|
from . import tree
|
||||||
if tree._is_updating:
|
if getattr(tree, "_is_updating", False):
|
||||||
return
|
return
|
||||||
tree._tree_changed = True
|
|
||||||
|
|
||||||
node = None
|
node = None
|
||||||
socket = None
|
socket = None
|
||||||
|
changed_tree = None
|
||||||
|
|
||||||
if isinstance(self, bpy.types.Node):
|
if _is_btype(self, "NodeTree"):
|
||||||
|
changed_tree = self
|
||||||
|
elif _is_btype(self, "Node"):
|
||||||
node = self
|
node = self
|
||||||
elif isinstance(self, bpy.types.NodeSocket):
|
changed_tree = getattr(self, "id_data", None)
|
||||||
|
elif _is_btype(self, "NodeSocket"):
|
||||||
socket = self
|
socket = self
|
||||||
node = self.node
|
node = getattr(self, "node", None)
|
||||||
elif isinstance(self, bpy.types.PropertyGroup):
|
changed_tree = getattr(self, "id_data", getattr(node, "id_data", None) if node else None)
|
||||||
|
elif _is_btype(self, "PropertyGroup"):
|
||||||
try:
|
try:
|
||||||
id_data = self.id_data
|
id_data = self.id_data
|
||||||
if isinstance(id_data, bpy.types.NodeTree):
|
if _is_btype(id_data, "NodeTree"):
|
||||||
|
changed_tree = id_data
|
||||||
path = self.path_from_id()
|
path = self.path_from_id()
|
||||||
if ".inputs[" in path or ".outputs[" in path:
|
if ".inputs[" in path or ".outputs[" in path:
|
||||||
socket_path = path.split(".items[")[0] if ".items[" in path else path.rsplit(".", 1)[0]
|
socket_path = path.split(".items[")[0] if ".items[" in path else path.rsplit(".", 1)[0]
|
||||||
resolved = id_data.path_resolve(socket_path)
|
resolved = id_data.path_resolve(socket_path)
|
||||||
if isinstance(resolved, bpy.types.NodeSocket):
|
if _is_btype(resolved, "NodeSocket"):
|
||||||
socket = resolved
|
socket = resolved
|
||||||
node = socket.node
|
node = socket.node
|
||||||
else:
|
else:
|
||||||
node_path = path.rsplit(".", 1)[0]
|
node_path = path.rsplit(".", 1)[0]
|
||||||
resolved = id_data.path_resolve(node_path)
|
resolved = id_data.path_resolve(node_path)
|
||||||
if isinstance(resolved, bpy.types.Node):
|
if _is_btype(resolved, "Node"):
|
||||||
node = resolved
|
node = resolved
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -41,6 +59,8 @@ def notify_tree_changed(self, context):
|
||||||
node = getattr(context, "node", None)
|
node = getattr(context, "node", None)
|
||||||
if socket is None:
|
if socket is None:
|
||||||
socket = getattr(context, "socket", None)
|
socket = getattr(context, "socket", None)
|
||||||
|
if changed_tree is None and node:
|
||||||
|
changed_tree = getattr(node, "id_data", None)
|
||||||
|
|
||||||
if socket and hasattr(socket, "update"):
|
if socket and hasattr(socket, "update"):
|
||||||
try:
|
try:
|
||||||
|
|
@ -54,6 +74,9 @@ def notify_tree_changed(self, context):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating node {node.name}: {e}")
|
print(f"Error updating node {node.name}: {e}")
|
||||||
|
|
||||||
|
if hasattr(tree, "request_auto_refresh"):
|
||||||
|
tree.request_auto_refresh(changed_tree)
|
||||||
|
|
||||||
class MockPropertyDescriptor:
|
class MockPropertyDescriptor:
|
||||||
def __init__(self, default: Any = None):
|
def __init__(self, default: Any = None):
|
||||||
self.default = default
|
self.default = default
|
||||||
|
|
|
||||||
|
|
@ -9,94 +9,303 @@ from ...registry import register_class
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from bpy.stub_internal.rna_enums import OperatorReturnItems
|
from bpy.stub_internal.rna_enums import OperatorReturnItems
|
||||||
|
|
||||||
def get_referenced_objects(node, visited=None):
|
def _get_scene_object(name: str) -> Any:
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
objects = getattr(bpy.data, "objects", None)
|
||||||
|
if objects is None:
|
||||||
|
return None
|
||||||
|
if isinstance(objects, dict):
|
||||||
|
return objects.get(name)
|
||||||
|
if "Mock" in type(objects).__name__:
|
||||||
|
return None
|
||||||
|
if hasattr(objects, "get"):
|
||||||
|
return objects.get(name)
|
||||||
|
if hasattr(objects, "__getitem__"):
|
||||||
|
try:
|
||||||
|
return objects[name]
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _is_btype(val: Any, type_name: str) -> bool:
|
||||||
|
if val is None:
|
||||||
|
return False
|
||||||
|
btype = getattr(bpy.types, type_name, None)
|
||||||
|
if isinstance(btype, type):
|
||||||
|
try:
|
||||||
|
if isinstance(val, btype):
|
||||||
|
return True
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
val_cls_name = getattr(val, "__class__", type(None)).__name__
|
||||||
|
return val_cls_name == type_name or f"Dummy{type_name}" in val_cls_name
|
||||||
|
|
||||||
|
def get_referenced_data(node: Any, visited: set[int] | None = None) -> tuple[set[Any], set[Any]]:
|
||||||
|
"""Traverse upstream from node and collect all referenced bpy.types.Object and bpy.types.Collection."""
|
||||||
if visited is None:
|
if visited is None:
|
||||||
visited = set()
|
visited = set()
|
||||||
|
|
||||||
ptr = node.as_pointer()
|
ptr = node.as_pointer() if hasattr(node, "as_pointer") and callable(node.as_pointer) else id(node)
|
||||||
|
if ptr in visited:
|
||||||
|
return set(), set()
|
||||||
|
visited.add(ptr)
|
||||||
|
|
||||||
|
objs: set[Any] = set()
|
||||||
|
cols: set[Any] = set()
|
||||||
|
|
||||||
|
# 1. Group Node: check internal nodes inside the group
|
||||||
|
if getattr(node, "bl_idname", "") == "SLICENODE_Group":
|
||||||
|
group_tree = getattr(node, "slicer_tree", None) or getattr(node, "node_tree", None)
|
||||||
|
if group_tree and hasattr(group_tree, "nodes"):
|
||||||
|
for internal_node in group_tree.nodes:
|
||||||
|
sub_objs, sub_cols = get_referenced_data(internal_node, visited)
|
||||||
|
objs.update(sub_objs)
|
||||||
|
cols.update(sub_cols)
|
||||||
|
|
||||||
|
# 2. CollectionObjects Node
|
||||||
|
if getattr(node, "bl_idname", "") == "SLICENODE_CollectionObjects":
|
||||||
|
col = getattr(node, "collection", None)
|
||||||
|
if col:
|
||||||
|
cols.add(col)
|
||||||
|
for o in getattr(col, "all_objects", getattr(col, "objects", [])):
|
||||||
|
if _is_btype(o, "Object"):
|
||||||
|
objs.add(o)
|
||||||
|
for outp in getattr(node, "outputs", []):
|
||||||
|
obj = _get_scene_object(getattr(outp, "identifier", "")) or _get_scene_object(getattr(outp, "name", ""))
|
||||||
|
if obj:
|
||||||
|
objs.add(obj)
|
||||||
|
|
||||||
|
# 3. Check node RNA pointer properties for Object or Collection
|
||||||
|
for prop in getattr(getattr(node, "bl_rna", None), "properties", []):
|
||||||
|
if getattr(prop, "type", "") == 'POINTER':
|
||||||
|
val = getattr(node, prop.identifier, None)
|
||||||
|
if _is_btype(val, "Object"):
|
||||||
|
objs.add(val)
|
||||||
|
elif _is_btype(val, "Collection"):
|
||||||
|
cols.add(val)
|
||||||
|
for o in getattr(val, "all_objects", getattr(val, "objects", [])):
|
||||||
|
if _is_btype(o, "Object"):
|
||||||
|
objs.add(o)
|
||||||
|
|
||||||
|
# 4. Check all input sockets
|
||||||
|
for inp in getattr(node, "inputs", []):
|
||||||
|
val = getattr(inp, "value", getattr(inp, "default_value", None))
|
||||||
|
if _is_btype(val, "Object"):
|
||||||
|
objs.add(val)
|
||||||
|
elif _is_btype(val, "Collection"):
|
||||||
|
cols.add(val)
|
||||||
|
|
||||||
|
# Follow upstream links
|
||||||
|
if getattr(inp, "is_linked", False):
|
||||||
|
for link in getattr(inp, "links", []):
|
||||||
|
from_node = getattr(link, "from_node", None)
|
||||||
|
if from_node:
|
||||||
|
if getattr(from_node, "bl_idname", "") == "SLICENODE_CollectionObjects":
|
||||||
|
from_sock = getattr(link, "from_socket", None)
|
||||||
|
if from_sock:
|
||||||
|
obj = _get_scene_object(getattr(from_sock, "identifier", "")) or _get_scene_object(getattr(from_sock, "name", ""))
|
||||||
|
if obj:
|
||||||
|
objs.add(obj)
|
||||||
|
sub_objs, sub_cols = get_referenced_data(from_node, visited)
|
||||||
|
objs.update(sub_objs)
|
||||||
|
cols.update(sub_cols)
|
||||||
|
|
||||||
|
# 5. Check all output sockets
|
||||||
|
for outp in getattr(node, "outputs", []):
|
||||||
|
val = getattr(outp, "value", getattr(outp, "default_value", None))
|
||||||
|
if _is_btype(val, "Object"):
|
||||||
|
objs.add(val)
|
||||||
|
elif _is_btype(val, "Collection"):
|
||||||
|
cols.add(val)
|
||||||
|
|
||||||
|
# 6. Expand all objects to include their recursive children and parents/ancestors
|
||||||
|
from ...infra.blender.blender_bridge import get_all_children
|
||||||
|
all_objs = set(objs)
|
||||||
|
for o in list(objs):
|
||||||
|
# Children
|
||||||
|
try:
|
||||||
|
for child in get_all_children(o):
|
||||||
|
all_objs.add(child)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Ancestors
|
||||||
|
curr = getattr(o, "parent", None)
|
||||||
|
seen_parents = set()
|
||||||
|
while curr and curr not in seen_parents and len(seen_parents) < 100:
|
||||||
|
seen_parents.add(curr)
|
||||||
|
all_objs.add(curr)
|
||||||
|
if "Mock" in type(curr).__name__:
|
||||||
|
break
|
||||||
|
curr = getattr(curr, "parent", None)
|
||||||
|
|
||||||
|
return all_objs, cols
|
||||||
|
|
||||||
|
def get_referenced_objects(node: Any, visited: set[int] | None = None) -> set[Any]:
|
||||||
|
objs, _ = get_referenced_data(node, visited)
|
||||||
|
return objs
|
||||||
|
|
||||||
|
def get_referenced_trees(node: Any, visited: set[int] | None = None) -> set[Any]:
|
||||||
|
"""Collect all NodeTree datablocks referenced upstream (e.g. via group nodes)."""
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
ptr = node.as_pointer() if hasattr(node, "as_pointer") and callable(node.as_pointer) else id(node)
|
||||||
if ptr in visited:
|
if ptr in visited:
|
||||||
return set()
|
return set()
|
||||||
visited.add(ptr)
|
visited.add(ptr)
|
||||||
|
|
||||||
objs = set()
|
trees = set()
|
||||||
|
if getattr(node, "bl_idname", "") == "SLICENODE_Group":
|
||||||
|
group_tree = getattr(node, "slicer_tree", None) or getattr(node, "node_tree", None)
|
||||||
|
if group_tree:
|
||||||
|
trees.add(group_tree)
|
||||||
|
if hasattr(group_tree, "nodes"):
|
||||||
|
for internal_node in group_tree.nodes:
|
||||||
|
trees.update(get_referenced_trees(internal_node, visited))
|
||||||
|
|
||||||
# Special case: if this is a group node, we must also check nodes inside the group
|
for inp in getattr(node, "inputs", []):
|
||||||
if node.bl_idname == "SLICENODE_Group" and getattr(node, "slicer_tree", None):
|
if getattr(inp, "is_linked", False):
|
||||||
for internal_node in node.slicer_tree.nodes:
|
for link in getattr(inp, "links", []):
|
||||||
objs.update(get_referenced_objects(internal_node, visited))
|
from_node = getattr(link, "from_node", None)
|
||||||
|
if from_node:
|
||||||
|
trees.update(get_referenced_trees(from_node, visited))
|
||||||
|
|
||||||
# Check all input sockets for objects
|
return trees
|
||||||
for inp in node.inputs:
|
|
||||||
# If it's an object socket, it might have a value if not linked
|
|
||||||
if inp.bl_idname == "NodeSocketObject":
|
|
||||||
if not inp.is_linked and getattr(inp, "value", getattr(inp, "default_value", None)):
|
|
||||||
val = getattr(inp, "value", getattr(inp, "default_value", None))
|
|
||||||
if isinstance(val, bpy.types.Object):
|
|
||||||
objs.add(val)
|
|
||||||
|
|
||||||
# Always follow links to find more objects upstream
|
|
||||||
if inp.is_linked:
|
|
||||||
for link in inp.links:
|
|
||||||
# Special case: if from_node is CollectionObjects, the object is identified by the socket identifier
|
|
||||||
if link.from_node.bl_idname == "SLICENODE_CollectionObjects":
|
|
||||||
obj = bpy.data.objects.get(link.from_socket.identifier)
|
|
||||||
if obj:
|
|
||||||
objs.add(obj)
|
|
||||||
|
|
||||||
objs.update(get_referenced_objects(link.from_node, visited))
|
|
||||||
|
|
||||||
# Also check output sockets of the node itself,
|
|
||||||
# as some nodes might "hold" an object (like InputObject)
|
|
||||||
for outp in node.outputs:
|
|
||||||
if outp.bl_idname == "NodeSocketObject":
|
|
||||||
val = getattr(outp, "value", getattr(outp, "default_value", None))
|
|
||||||
if isinstance(val, bpy.types.Object):
|
|
||||||
objs.add(val)
|
|
||||||
elif node.bl_idname == "SLICENODE_CollectionObjects" and outp.is_linked:
|
|
||||||
obj = bpy.data.objects.get(outp.identifier)
|
|
||||||
if obj:
|
|
||||||
objs.add(obj)
|
|
||||||
|
|
||||||
return objs
|
|
||||||
|
|
||||||
_is_updating = False
|
_is_updating = False
|
||||||
_tree_changed = False
|
_tree_changed = False
|
||||||
|
_changed_trees: set[Any] = set()
|
||||||
|
_timer_scheduled = False
|
||||||
|
|
||||||
def tree_update_handler(scene, depsgraph):
|
def _has_real_timers() -> bool:
|
||||||
global _tree_changed, _is_updating
|
timers = getattr(bpy.app, "timers", None)
|
||||||
|
if timers is None:
|
||||||
|
return False
|
||||||
|
if "Mock" in type(timers).__name__:
|
||||||
|
return False
|
||||||
|
reg = getattr(timers, "register", None)
|
||||||
|
if reg is None or "Mock" in type(reg).__name__:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
# Names of objects that have changed
|
def cancel_timer() -> None:
|
||||||
updated_object_names = {update.id.name for update in depsgraph.updates if isinstance(update.id, bpy.types.Object)}
|
global _timer_scheduled
|
||||||
|
if _timer_scheduled and hasattr(bpy.app, "timers") and hasattr(bpy.app.timers, "unregister"):
|
||||||
|
try:
|
||||||
|
bpy.app.timers.unregister(_timer_callback)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_timer_scheduled = False
|
||||||
|
|
||||||
# Also check for mesh updates (happens in Edit Mode)
|
def _timer_callback() -> float | None:
|
||||||
updated_meshes = {update.id.name for update in depsgraph.updates if isinstance(update.id, bpy.types.Mesh)}
|
global _timer_scheduled, _is_updating
|
||||||
if updated_meshes:
|
if _is_updating:
|
||||||
for obj in bpy.data.objects:
|
# Retry in 50ms if currently evaluating
|
||||||
if obj.type == 'MESH' and obj.data.name in updated_meshes:
|
return 0.05
|
||||||
updated_object_names.add(obj.name)
|
_timer_scheduled = False
|
||||||
|
run_auto_refresh()
|
||||||
|
return None
|
||||||
|
|
||||||
# Note: custom NodeTree updates often don't appear in depsgraph.updates
|
def schedule_auto_refresh(delay: float = 0.02) -> None:
|
||||||
# so we rely on _tree_changed flag set by property update callbacks.
|
global _timer_scheduled
|
||||||
|
if _timer_scheduled:
|
||||||
|
return
|
||||||
|
if _has_real_timers():
|
||||||
|
try:
|
||||||
|
_timer_scheduled = True
|
||||||
|
bpy.app.timers.register(_timer_callback, first_interval=delay, persistent=True)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
_timer_scheduled = False
|
||||||
|
# Fallback if timers are not running / available (e.g. tests or non-interactive CLI)
|
||||||
|
if not _is_updating:
|
||||||
|
run_auto_refresh()
|
||||||
|
|
||||||
if not updated_object_names and not _tree_changed:
|
def request_auto_refresh(changed_tree: Any = None) -> None:
|
||||||
|
global _tree_changed, _changed_trees
|
||||||
|
if _is_updating:
|
||||||
|
return
|
||||||
|
_tree_changed = True
|
||||||
|
if changed_tree is not None:
|
||||||
|
_changed_trees.add(changed_tree)
|
||||||
|
schedule_auto_refresh()
|
||||||
|
|
||||||
|
def _tag_redraw() -> None:
|
||||||
|
try:
|
||||||
|
wm = getattr(bpy.context, "window_manager", None)
|
||||||
|
if wm:
|
||||||
|
for window in getattr(wm, "windows", []):
|
||||||
|
screen = getattr(window, "screen", None)
|
||||||
|
if screen:
|
||||||
|
for area in getattr(screen, "areas", []):
|
||||||
|
if getattr(area, "type", "") in ('VIEW_3D', 'NODE_EDITOR'):
|
||||||
|
area.tag_redraw()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run_auto_refresh(
|
||||||
|
updated_object_names: set[str] | None = None,
|
||||||
|
updated_collection_names: set[str] | None = None,
|
||||||
|
changed_trees: set[Any] | None = None,
|
||||||
|
force_refresh: bool = False
|
||||||
|
) -> None:
|
||||||
|
global _is_updating, _tree_changed, _changed_trees
|
||||||
|
|
||||||
|
if _is_updating:
|
||||||
return
|
return
|
||||||
|
|
||||||
tree_needs_refresh = _tree_changed
|
active_changed_trees = set(_changed_trees)
|
||||||
_tree_changed = False # reset for next run
|
if changed_trees:
|
||||||
|
active_changed_trees.update(changed_trees)
|
||||||
|
_changed_trees.clear()
|
||||||
|
|
||||||
for tree in bpy.data.node_groups:
|
tree_needs_refresh = force_refresh or _tree_changed
|
||||||
if tree.bl_idname != "SLICETREE_NodeTree":
|
_tree_changed = False
|
||||||
|
|
||||||
|
has_updates = tree_needs_refresh or bool(updated_object_names) or bool(updated_collection_names)
|
||||||
|
if not has_updates:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Collect all SLICETREE_NodeTree datablocks
|
||||||
|
trees_to_check: list[Any] = []
|
||||||
|
node_groups = getattr(bpy.data, "node_groups", None)
|
||||||
|
if node_groups is not None:
|
||||||
|
trees_to_check.extend(list(node_groups))
|
||||||
|
try:
|
||||||
|
space_tree = getattr(getattr(bpy.context, "space_data", None), "edit_tree", None)
|
||||||
|
if space_tree and space_tree not in trees_to_check:
|
||||||
|
trees_to_check.append(space_tree)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
executed_any = False
|
||||||
|
for tree in trees_to_check:
|
||||||
|
if getattr(tree, "bl_idname", "") != "SLICETREE_NodeTree":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for node in tree.nodes:
|
for node in getattr(tree, "nodes", []):
|
||||||
if node.bl_idname == "SLICENODE_Executor" and getattr(node, "auto_refresh", False):
|
if getattr(node, "bl_idname", "") == "SLICENODE_Executor" and getattr(node, "auto_refresh", False):
|
||||||
# Trigger if the tree changed OR if a referenced object changed
|
should_refresh = False
|
||||||
should_refresh = tree_needs_refresh
|
|
||||||
|
if tree_needs_refresh:
|
||||||
|
if not active_changed_trees:
|
||||||
|
should_refresh = True
|
||||||
|
else:
|
||||||
|
owner_tree = getattr(node, "id_data", tree)
|
||||||
|
dep_trees = {owner_tree} | get_referenced_trees(node)
|
||||||
|
if active_changed_trees & dep_trees:
|
||||||
|
should_refresh = True
|
||||||
|
|
||||||
if not should_refresh:
|
if not should_refresh:
|
||||||
referenced = get_referenced_objects(node)
|
ref_objs, ref_cols = get_referenced_data(node)
|
||||||
referenced_names = {obj.name for obj in referenced if obj}
|
if updated_object_names:
|
||||||
if updated_object_names & referenced_names:
|
ref_names = {o.name for o in ref_objs if o and hasattr(o, "name")}
|
||||||
|
if updated_object_names & ref_names:
|
||||||
|
should_refresh = True
|
||||||
|
if not should_refresh and updated_collection_names:
|
||||||
|
ref_col_names = {c.name for c in ref_cols if c and hasattr(c, "name")}
|
||||||
|
if updated_collection_names & ref_col_names:
|
||||||
should_refresh = True
|
should_refresh = True
|
||||||
|
|
||||||
if should_refresh:
|
if should_refresh:
|
||||||
|
|
@ -104,6 +313,7 @@ def tree_update_handler(scene, depsgraph):
|
||||||
try:
|
try:
|
||||||
with execution_cache():
|
with execution_cache():
|
||||||
node.evaluate()
|
node.evaluate()
|
||||||
|
executed_any = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Auto-refresh failed: {e}")
|
print(f"Auto-refresh failed: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
@ -111,6 +321,43 @@ def tree_update_handler(scene, depsgraph):
|
||||||
finally:
|
finally:
|
||||||
_is_updating = False
|
_is_updating = False
|
||||||
|
|
||||||
|
if executed_any:
|
||||||
|
_tag_redraw()
|
||||||
|
|
||||||
|
def tree_update_handler(scene, depsgraph):
|
||||||
|
global _tree_changed, _is_updating
|
||||||
|
if _is_updating:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Names of objects that have changed
|
||||||
|
updated_object_names = {update.id.name for update in depsgraph.updates if _is_btype(update.id, "Object")}
|
||||||
|
|
||||||
|
# Also check for mesh updates (happens in Edit Mode)
|
||||||
|
updated_meshes = {update.id.name for update in depsgraph.updates if _is_btype(update.id, "Mesh")}
|
||||||
|
if updated_meshes:
|
||||||
|
objects = getattr(bpy.data, "objects", [])
|
||||||
|
for obj in objects:
|
||||||
|
if getattr(obj, "type", "") == 'MESH' and getattr(getattr(obj, "data", None), "name", None) in updated_meshes:
|
||||||
|
updated_object_names.add(obj.name)
|
||||||
|
|
||||||
|
# Check for collection updates
|
||||||
|
updated_collection_names = {update.id.name for update in depsgraph.updates if _is_btype(update.id, "Collection")}
|
||||||
|
|
||||||
|
# Check if any NodeTree updated in depsgraph
|
||||||
|
updated_trees = {update.id for update in depsgraph.updates if _is_btype(update.id, "NodeTree")}
|
||||||
|
changed_trees = {t for t in updated_trees if getattr(t, "bl_idname", "") == "SLICETREE_NodeTree"}
|
||||||
|
if changed_trees:
|
||||||
|
_tree_changed = True
|
||||||
|
|
||||||
|
if not updated_object_names and not updated_collection_names and not _tree_changed:
|
||||||
|
return
|
||||||
|
|
||||||
|
run_auto_refresh(
|
||||||
|
updated_object_names=updated_object_names,
|
||||||
|
updated_collection_names=updated_collection_names,
|
||||||
|
changed_trees=changed_trees
|
||||||
|
)
|
||||||
|
|
||||||
@persistent
|
@persistent
|
||||||
def depsgraph_update_handler(scene, depsgraph):
|
def depsgraph_update_handler(scene, depsgraph):
|
||||||
global _is_updating, _tree_changed
|
global _is_updating, _tree_changed
|
||||||
|
|
@ -158,6 +405,17 @@ class SLICETREE_NodeTree(NodeTree):
|
||||||
bl_group_idname = "SLICENODE_Group"
|
bl_group_idname = "SLICENODE_Group"
|
||||||
bl_use_group_interface = True
|
bl_use_group_interface = True
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
super_update = getattr(super(), "update", None)
|
||||||
|
if callable(super_update):
|
||||||
|
try:
|
||||||
|
super_update()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if _is_updating:
|
||||||
|
return
|
||||||
|
request_auto_refresh(self)
|
||||||
|
|
||||||
def valid_link(self, from_socket: Any, to_socket: Any) -> bool:
|
def valid_link(self, from_socket: Any, to_socket: Any) -> bool:
|
||||||
geom_sockets = {"SLICESOCK_Mesh", "SLICESOCK_Paths", "SLICESOCK_Geometry"}
|
geom_sockets = {"SLICESOCK_Mesh", "SLICESOCK_Paths", "SLICESOCK_Geometry"}
|
||||||
out_s, in_s = from_socket, to_socket
|
out_s, in_s = from_socket, to_socket
|
||||||
|
|
@ -232,7 +490,7 @@ class SLICENODE_OT_GroupToggle(Operator):
|
||||||
|
|
||||||
# If we have a target group node, we try to enter it
|
# If we have a target group node, we try to enter it
|
||||||
if target_node and target_node.bl_idname == "SLICENODE_Group":
|
if target_node and target_node.bl_idname == "SLICENODE_Group":
|
||||||
group_tree = getattr(target_node, "slicer_tree", None)
|
group_tree = getattr(target_node, "slicer_tree", None) or getattr(target_node, "node_tree", None)
|
||||||
if group_tree:
|
if group_tree:
|
||||||
path.append(group_tree, node=target_node)
|
path.append(group_tree, node=target_node)
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
def get_all_children(obj):
|
def get_all_children(obj, visited=None):
|
||||||
|
if visited is None:
|
||||||
|
visited = set()
|
||||||
children = []
|
children = []
|
||||||
for child in getattr(obj, "children", []):
|
children_attr = getattr(obj, "children", [])
|
||||||
children += [child] + get_all_children(child)
|
if "Mock" in type(children_attr).__name__ or not isinstance(children_attr, (list, tuple, set)):
|
||||||
|
return []
|
||||||
children = list(set(children))
|
for child in children_attr:
|
||||||
return children
|
if child not in visited:
|
||||||
|
visited.add(child)
|
||||||
|
children.append(child)
|
||||||
|
children.extend(get_all_children(child, visited))
|
||||||
|
return list(set(children))
|
||||||
357
tests/test_auto_refresh.py
Normal file
357
tests/test_auto_refresh.py
Normal file
|
|
@ -0,0 +1,357 @@
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
sys.path.append(str(PROJECT_ROOT.parent))
|
||||||
|
|
||||||
|
class DummyObject:
|
||||||
|
def __init__(self, name="TestObj", obj_type='MESH', parent=None, children=None):
|
||||||
|
self.name = name
|
||||||
|
self.type = obj_type
|
||||||
|
self.parent = parent
|
||||||
|
self.children = children or []
|
||||||
|
|
||||||
|
def as_pointer(self):
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
class DummyCollection:
|
||||||
|
def __init__(self, name="Col", objects=None):
|
||||||
|
self.name = name
|
||||||
|
self.objects = objects or []
|
||||||
|
self.all_objects = self.objects
|
||||||
|
|
||||||
|
def as_pointer(self):
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
if 'bpy' not in sys.modules:
|
||||||
|
class DummyBase: pass
|
||||||
|
mock_types = MagicMock()
|
||||||
|
mock_types.NodeSocket = type("NodeSocket", (DummyBase,), {})
|
||||||
|
mock_types.Node = type("Node", (DummyBase,), {})
|
||||||
|
mock_types.NodeCustomGroup = type("NodeCustomGroup", (DummyBase,), {})
|
||||||
|
mock_types.NodeTree = type("NodeTree", (DummyBase,), {})
|
||||||
|
mock_types.PropertyGroup = type("PropertyGroup", (DummyBase,), {})
|
||||||
|
mock_types.Operator = type("Operator", (DummyBase,), {})
|
||||||
|
mock_types.Panel = type("Panel", (DummyBase,), {})
|
||||||
|
mock_types.Menu = type("Menu", (DummyBase,), {})
|
||||||
|
mock_types.Object = DummyObject
|
||||||
|
mock_types.Collection = DummyCollection
|
||||||
|
|
||||||
|
mock_bpy = MagicMock()
|
||||||
|
mock_bpy.types = mock_types
|
||||||
|
sys.modules['bpy'] = mock_bpy
|
||||||
|
sys.modules['bpy.types'] = mock_types
|
||||||
|
sys.modules['bpy.props'] = MagicMock()
|
||||||
|
sys.modules['bpy.utils'] = MagicMock()
|
||||||
|
sys.modules['bpy.utils.previews'] = MagicMock()
|
||||||
|
sys.modules['bpy.app'] = MagicMock()
|
||||||
|
sys.modules['bpy.app.handlers'] = MagicMock()
|
||||||
|
else:
|
||||||
|
if 'bpy.props' not in sys.modules:
|
||||||
|
sys.modules['bpy.props'] = MagicMock()
|
||||||
|
|
||||||
|
class DummyNodeSocket:
|
||||||
|
def __init__(self, name="", bl_idname="NodeSocketObject", node=None, value=None):
|
||||||
|
self.name = name
|
||||||
|
self.identifier = name
|
||||||
|
self.bl_idname = bl_idname
|
||||||
|
self.node = node
|
||||||
|
self.is_linked = False
|
||||||
|
self.links = []
|
||||||
|
self.value = value
|
||||||
|
self.default_value = value
|
||||||
|
|
||||||
|
def as_pointer(self):
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
class DummyLink:
|
||||||
|
def __init__(self, from_socket, to_socket):
|
||||||
|
self.from_socket = from_socket
|
||||||
|
self.from_node = getattr(from_socket, "node", None)
|
||||||
|
self.to_socket = to_socket
|
||||||
|
self.to_node = getattr(to_socket, "node", None)
|
||||||
|
|
||||||
|
class DummyNode:
|
||||||
|
def __init__(self, name="Node", bl_idname="DummyNode", id_data=None):
|
||||||
|
self.name = name
|
||||||
|
self.bl_idname = bl_idname
|
||||||
|
self.id_data = id_data
|
||||||
|
self.inputs = []
|
||||||
|
self.outputs = []
|
||||||
|
self.bl_rna = MagicMock()
|
||||||
|
self.bl_rna.properties = []
|
||||||
|
|
||||||
|
def as_pointer(self):
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
class DummyGroupNode(DummyNode):
|
||||||
|
def __init__(self, name="GroupNode", slicer_tree=None, node_tree=None, id_data=None):
|
||||||
|
super().__init__(name, "SLICENODE_Group", id_data)
|
||||||
|
self.slicer_tree = slicer_tree
|
||||||
|
self.node_tree = node_tree
|
||||||
|
|
||||||
|
class DummyExecutorNode(DummyNode):
|
||||||
|
def __init__(self, name="Executor", auto_refresh=True, id_data=None):
|
||||||
|
super().__init__(name, "SLICENODE_Executor", id_data)
|
||||||
|
self.auto_refresh = auto_refresh
|
||||||
|
self.evaluated = False
|
||||||
|
self.evaluate_count = 0
|
||||||
|
|
||||||
|
def evaluate(self, operator=None):
|
||||||
|
self.evaluated = True
|
||||||
|
self.evaluate_count += 1
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
class DummyNodeTree:
|
||||||
|
def __init__(self, name="Tree"):
|
||||||
|
self.name = name
|
||||||
|
self.bl_idname = "SLICETREE_NodeTree"
|
||||||
|
self.nodes = []
|
||||||
|
|
||||||
|
def as_pointer(self):
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
|
||||||
|
# Import tree functions
|
||||||
|
from NodeSlicer.core.nodeslicer.tree import (
|
||||||
|
get_referenced_data,
|
||||||
|
get_referenced_objects,
|
||||||
|
get_referenced_trees,
|
||||||
|
run_auto_refresh,
|
||||||
|
request_auto_refresh,
|
||||||
|
SLICETREE_NodeTree
|
||||||
|
)
|
||||||
|
import NodeSlicer.core.nodeslicer.tree as tree_mod
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
|
||||||
|
class TestAutoRefresh(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
tree_mod._is_updating = False
|
||||||
|
tree_mod._tree_changed = False
|
||||||
|
tree_mod._changed_trees.clear()
|
||||||
|
tree_mod._timer_scheduled = False
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
tree_mod._is_updating = False
|
||||||
|
tree_mod._tree_changed = False
|
||||||
|
tree_mod._changed_trees.clear()
|
||||||
|
tree_mod._timer_scheduled = False
|
||||||
|
|
||||||
|
def test_referenced_objects_direct_and_upstream(self):
|
||||||
|
obj1 = DummyObject("Obj1")
|
||||||
|
obj2 = DummyObject("Obj2")
|
||||||
|
|
||||||
|
node_a = DummyNode("NodeA")
|
||||||
|
out_sock_a = DummyNodeSocket("Out", "NodeSocketObject", node=node_a, value=obj1)
|
||||||
|
node_a.outputs.append(out_sock_a)
|
||||||
|
|
||||||
|
node_b = DummyNode("NodeB")
|
||||||
|
in_sock_b = DummyNodeSocket("In", "NodeSocketObject", node=node_b)
|
||||||
|
node_b.inputs.append(in_sock_b)
|
||||||
|
|
||||||
|
# Link A to B
|
||||||
|
link = DummyLink(out_sock_a, in_sock_b)
|
||||||
|
in_sock_b.is_linked = True
|
||||||
|
in_sock_b.links.append(link)
|
||||||
|
|
||||||
|
# Direct unlinked socket on B with obj2
|
||||||
|
in_sock_b2 = DummyNodeSocket("In2", "SLICESOCK_Object", node=node_b, value=obj2)
|
||||||
|
node_b.inputs.append(in_sock_b2)
|
||||||
|
|
||||||
|
ref_objs = get_referenced_objects(node_b)
|
||||||
|
self.assertIn(obj1, ref_objs)
|
||||||
|
self.assertIn(obj2, ref_objs)
|
||||||
|
|
||||||
|
def test_referenced_objects_with_hierarchy(self):
|
||||||
|
parent_empty = DummyObject("ParentRig")
|
||||||
|
child_mesh = DummyObject("ChildDetail")
|
||||||
|
main_obj = DummyObject("MainMesh", parent=parent_empty, children=[child_mesh])
|
||||||
|
parent_empty.children = [main_obj]
|
||||||
|
child_mesh.parent = main_obj
|
||||||
|
|
||||||
|
node = DummyNode("MeshNode")
|
||||||
|
sock = DummyNodeSocket("Obj", "NodeSocketObject", node=node, value=main_obj)
|
||||||
|
node.inputs.append(sock)
|
||||||
|
|
||||||
|
ref_objs = get_referenced_objects(node)
|
||||||
|
self.assertIn(main_obj, ref_objs)
|
||||||
|
self.assertIn(parent_empty, ref_objs)
|
||||||
|
self.assertIn(child_mesh, ref_objs)
|
||||||
|
|
||||||
|
def test_referenced_objects_in_node_groups(self):
|
||||||
|
obj_inner = DummyObject("InnerSuzanne")
|
||||||
|
obj_outer = DummyObject("OuterCube")
|
||||||
|
|
||||||
|
# Group tree
|
||||||
|
group_tree = DummyNodeTree("GroupTree")
|
||||||
|
inner_node = DummyNode("InnerNode")
|
||||||
|
inner_sock = DummyNodeSocket("Obj", "NodeSocketObject", node=inner_node, value=obj_inner)
|
||||||
|
inner_node.inputs.append(inner_sock)
|
||||||
|
group_tree.nodes.append(inner_node)
|
||||||
|
|
||||||
|
# Group Node instance
|
||||||
|
group_node = DummyGroupNode("GroupInstance", slicer_tree=group_tree)
|
||||||
|
outer_sock = DummyNodeSocket("OuterIn", "NodeSocketObject", node=group_node, value=obj_outer)
|
||||||
|
group_node.inputs.append(outer_sock)
|
||||||
|
|
||||||
|
# Main node connected to group node
|
||||||
|
main_node = DummyNode("MainNode")
|
||||||
|
main_in = DummyNodeSocket("In", "SLICESOCK_Mesh", node=main_node)
|
||||||
|
group_out = DummyNodeSocket("Out", "SLICESOCK_Mesh", node=group_node)
|
||||||
|
group_node.outputs.append(group_out)
|
||||||
|
|
||||||
|
link = DummyLink(group_out, main_in)
|
||||||
|
main_in.is_linked = True
|
||||||
|
main_in.links.append(link)
|
||||||
|
main_node.inputs.append(main_in)
|
||||||
|
|
||||||
|
ref_objs = get_referenced_objects(main_node)
|
||||||
|
self.assertIn(obj_inner, ref_objs)
|
||||||
|
self.assertIn(obj_outer, ref_objs)
|
||||||
|
|
||||||
|
def test_referenced_objects_nested_groups(self):
|
||||||
|
obj_deep = DummyObject("DeepObj")
|
||||||
|
|
||||||
|
# Inner-most group
|
||||||
|
deep_tree = DummyNodeTree("DeepTree")
|
||||||
|
deep_node = DummyNode("DeepNode")
|
||||||
|
deep_node.inputs.append(DummyNodeSocket("DeepSock", value=obj_deep))
|
||||||
|
deep_tree.nodes.append(deep_node)
|
||||||
|
|
||||||
|
# Middle group containing inner group
|
||||||
|
mid_tree = DummyNodeTree("MidTree")
|
||||||
|
mid_group = DummyGroupNode("MidGroupNode", node_tree=deep_tree)
|
||||||
|
mid_tree.nodes.append(mid_group)
|
||||||
|
|
||||||
|
# Top group node
|
||||||
|
top_group = DummyGroupNode("TopGroupNode", slicer_tree=mid_tree)
|
||||||
|
|
||||||
|
ref_objs = get_referenced_objects(top_group)
|
||||||
|
self.assertIn(obj_deep, ref_objs)
|
||||||
|
|
||||||
|
def test_get_referenced_trees(self):
|
||||||
|
# Two group trees: GroupA and GroupB
|
||||||
|
tree_b = DummyNodeTree("TreeB")
|
||||||
|
node_b = DummyGroupNode("NodeB", slicer_tree=tree_b)
|
||||||
|
|
||||||
|
tree_a = DummyNodeTree("TreeA")
|
||||||
|
node_a = DummyGroupNode("NodeA", slicer_tree=tree_a)
|
||||||
|
sock_a = DummyNodeSocket("A_Out", node=node_a)
|
||||||
|
node_a.outputs.append(sock_a)
|
||||||
|
|
||||||
|
executor = DummyExecutorNode("Executor")
|
||||||
|
exec_in = DummyNodeSocket("In", node=executor)
|
||||||
|
exec_in.is_linked = True
|
||||||
|
exec_in.links.append(DummyLink(sock_a, exec_in))
|
||||||
|
executor.inputs.append(exec_in)
|
||||||
|
|
||||||
|
trees = get_referenced_trees(executor)
|
||||||
|
self.assertIn(tree_a, trees)
|
||||||
|
self.assertNotIn(tree_b, trees)
|
||||||
|
|
||||||
|
def test_collection_objects_referenced(self):
|
||||||
|
obj_c1 = DummyObject("C1")
|
||||||
|
obj_c2 = DummyObject("C2")
|
||||||
|
col = DummyCollection("TestCollection", [obj_c1, obj_c2])
|
||||||
|
|
||||||
|
col_node = DummyNode("ColNode", bl_idname="SLICENODE_CollectionObjects")
|
||||||
|
col_node.collection = col
|
||||||
|
|
||||||
|
# Output sockets named C1, C2
|
||||||
|
out_sock1 = DummyNodeSocket("C1", node=col_node)
|
||||||
|
out_sock2 = DummyNodeSocket("C2", node=col_node)
|
||||||
|
col_node.outputs.extend([out_sock1, out_sock2])
|
||||||
|
|
||||||
|
ref_objs, ref_cols = get_referenced_data(col_node)
|
||||||
|
self.assertIn(col, ref_cols)
|
||||||
|
self.assertIn(obj_c1, ref_objs)
|
||||||
|
self.assertIn(obj_c2, ref_objs)
|
||||||
|
|
||||||
|
def test_run_auto_refresh_on_object_change(self):
|
||||||
|
main_tree = DummyNodeTree("MainTree")
|
||||||
|
executor = DummyExecutorNode("Executor", auto_refresh=True, id_data=main_tree)
|
||||||
|
target_obj = DummyObject("TargetCube")
|
||||||
|
|
||||||
|
in_sock = DummyNodeSocket("MeshIn", node=executor, value=target_obj)
|
||||||
|
executor.inputs.append(in_sock)
|
||||||
|
main_tree.nodes.append(executor)
|
||||||
|
|
||||||
|
# Mock bpy.data.node_groups
|
||||||
|
old_groups = getattr(tree_mod.bpy.data, "node_groups", None)
|
||||||
|
tree_mod.bpy.data.node_groups = [main_tree]
|
||||||
|
bpy.data.node_groups = [main_tree]
|
||||||
|
try:
|
||||||
|
# 1. Unrelated object update -> should NOT evaluate
|
||||||
|
run_auto_refresh(updated_object_names={"UnrelatedObject"})
|
||||||
|
self.assertFalse(executor.evaluated)
|
||||||
|
|
||||||
|
# 2. Related object update -> SHOULD evaluate
|
||||||
|
run_auto_refresh(updated_object_names={"TargetCube"})
|
||||||
|
self.assertTrue(executor.evaluated)
|
||||||
|
self.assertEqual(executor.evaluate_count, 1)
|
||||||
|
|
||||||
|
# 3. auto_refresh = False -> should NOT evaluate
|
||||||
|
executor.evaluated = False
|
||||||
|
executor.auto_refresh = False
|
||||||
|
run_auto_refresh(updated_object_names={"TargetCube"})
|
||||||
|
self.assertFalse(executor.evaluated)
|
||||||
|
finally:
|
||||||
|
tree_mod.bpy.data.node_groups = old_groups
|
||||||
|
bpy.data.node_groups = old_groups
|
||||||
|
|
||||||
|
def test_run_auto_refresh_on_group_tree_change(self):
|
||||||
|
group_tree = DummyNodeTree("GroupTree")
|
||||||
|
main_tree = DummyNodeTree("MainTree")
|
||||||
|
unrelated_tree = DummyNodeTree("UnrelatedTree")
|
||||||
|
|
||||||
|
group_node = DummyGroupNode("GroupInstance", slicer_tree=group_tree, id_data=main_tree)
|
||||||
|
g_out = DummyNodeSocket("Out", node=group_node)
|
||||||
|
group_node.outputs.append(g_out)
|
||||||
|
|
||||||
|
executor = DummyExecutorNode("Executor", auto_refresh=True, id_data=main_tree)
|
||||||
|
e_in = DummyNodeSocket("In", node=executor)
|
||||||
|
e_in.is_linked = True
|
||||||
|
e_in.links.append(DummyLink(g_out, e_in))
|
||||||
|
executor.inputs.append(e_in)
|
||||||
|
main_tree.nodes.extend([group_node, executor])
|
||||||
|
|
||||||
|
old_groups = getattr(tree_mod.bpy.data, "node_groups", None)
|
||||||
|
tree_mod.bpy.data.node_groups = [main_tree, group_tree, unrelated_tree]
|
||||||
|
bpy.data.node_groups = [main_tree, group_tree, unrelated_tree]
|
||||||
|
try:
|
||||||
|
# 1. Change to unrelated tree -> should NOT evaluate
|
||||||
|
run_auto_refresh(changed_trees={unrelated_tree}, force_refresh=True)
|
||||||
|
self.assertFalse(executor.evaluated)
|
||||||
|
|
||||||
|
# 2. Change to group_tree -> SHOULD evaluate
|
||||||
|
run_auto_refresh(changed_trees={group_tree}, force_refresh=True)
|
||||||
|
self.assertTrue(executor.evaluated)
|
||||||
|
self.assertEqual(executor.evaluate_count, 1)
|
||||||
|
|
||||||
|
# 3. Change to main_tree -> SHOULD evaluate
|
||||||
|
executor.evaluated = False
|
||||||
|
run_auto_refresh(changed_trees={main_tree}, force_refresh=True)
|
||||||
|
self.assertTrue(executor.evaluated)
|
||||||
|
self.assertEqual(executor.evaluate_count, 2)
|
||||||
|
finally:
|
||||||
|
tree_mod.bpy.data.node_groups = old_groups
|
||||||
|
bpy.data.node_groups = old_groups
|
||||||
|
|
||||||
|
def test_request_auto_refresh_guard(self):
|
||||||
|
# When _is_updating is True, request_auto_refresh must do nothing
|
||||||
|
tree_mod._is_updating = True
|
||||||
|
tree_mod._tree_changed = False
|
||||||
|
tree_mod._timer_scheduled = True
|
||||||
|
request_auto_refresh()
|
||||||
|
self.assertFalse(tree_mod._tree_changed)
|
||||||
|
|
||||||
|
tree_mod._is_updating = False
|
||||||
|
request_auto_refresh()
|
||||||
|
self.assertTrue(tree_mod._tree_changed)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
@ -164,6 +164,8 @@ class TestCollectionObjectsSorting(unittest.TestCase):
|
||||||
def test_evaluate_output(self):
|
def test_evaluate_output(self):
|
||||||
obj_a = DummyObject("Apple", 'MESH')
|
obj_a = DummyObject("Apple", 'MESH')
|
||||||
bpy.data.objects = {"Apple": obj_a}
|
bpy.data.objects = {"Apple": obj_a}
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes import inputs as inp_module
|
||||||
|
inp_module.bpy.data.objects = {"Apple": obj_a}
|
||||||
collection = DummyCollection("Fruits", [obj_a])
|
collection = DummyCollection("Fruits", [obj_a])
|
||||||
self.node.collection = collection
|
self.node.collection = collection
|
||||||
self.node._update_outputs()
|
self.node._update_outputs()
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,9 @@ mock_types.NodeGroupInput = DummyNodeGroupInput
|
||||||
mock_types.NodeGroupOutput = DummyNodeGroupOutput
|
mock_types.NodeGroupOutput = DummyNodeGroupOutput
|
||||||
mock_types.NodeCustomGroup = DummyGroupNode
|
mock_types.NodeCustomGroup = DummyGroupNode
|
||||||
|
|
||||||
|
if 'bpy' not in sys.modules:
|
||||||
mock_bpy = MagicMock()
|
mock_bpy = MagicMock()
|
||||||
mock_bpy.types = mock_types
|
mock_bpy.types = mock_types
|
||||||
|
|
||||||
sys.modules['bpy'] = mock_bpy
|
sys.modules['bpy'] = mock_bpy
|
||||||
sys.modules['bpy.types'] = mock_types
|
sys.modules['bpy.types'] = mock_types
|
||||||
sys.modules['bpy.props'] = MagicMock()
|
sys.modules['bpy.props'] = MagicMock()
|
||||||
|
|
@ -66,6 +66,14 @@ sys.modules['bpy.utils'] = MagicMock()
|
||||||
sys.modules['bpy.utils.previews'] = MagicMock()
|
sys.modules['bpy.utils.previews'] = MagicMock()
|
||||||
sys.modules['bpy.app'] = MagicMock()
|
sys.modules['bpy.app'] = MagicMock()
|
||||||
sys.modules['bpy.app.handlers'] = MagicMock()
|
sys.modules['bpy.app.handlers'] = MagicMock()
|
||||||
|
else:
|
||||||
|
mock_bpy = sys.modules['bpy']
|
||||||
|
mock_bpy.types.NodeSocket = DummyNodeSocket
|
||||||
|
mock_bpy.types.Node = DummyNode
|
||||||
|
mock_bpy.types.NodeGroup = DummyNodeGroup
|
||||||
|
mock_bpy.types.NodeGroupInput = DummyNodeGroupInput
|
||||||
|
mock_bpy.types.NodeGroupOutput = DummyNodeGroupOutput
|
||||||
|
mock_bpy.types.NodeCustomGroup = DummyGroupNode
|
||||||
|
|
||||||
from NodeSlicer.infra.blender.blender_slice_nodes_generic import (
|
from NodeSlicer.infra.blender.blender_slice_nodes_generic import (
|
||||||
_resolve_group_output,
|
_resolve_group_output,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue