nodeslicer/infra/blender/blender_slice_nodes_generic.py
2026-10-14 16:39:49 +03:00

1007 lines
38 KiB
Python

from __future__ import annotations
from dataclasses import field
import time
import typing
from typing import Any, cast
import bpy
from contextlib import contextmanager
from bpy.types import NodeSocket, Node
class NodeException(Exception): ...
from .list_utils import is_list_data, flatten_list, eval_element_wise, set_socket_shape, is_socket_linked_to_list, sync_node_list_sockets
_EvalCache = dict[Any, Any]
_VisitedSet = set[Any]
NATIVE_SOCKET_MAP: dict[str, str] = {}
NATIVE_SOCKET_LABELS: dict[str, str] = {
"NodeSocketFloat": "Float",
"NodeSocketInt": "Integer",
"NodeSocketVector": "Vector",
"NodeSocketRotation": "Rotation",
"NodeSocketString": "String",
"NodeSocketObject": "Object",
}
if hasattr(bpy.types, "NodeSocket") and not hasattr(bpy.types.NodeSocket, "value"):
def _get_socket_value(self):
try:
return self.default_value
except (AttributeError, TypeError):
pass
return getattr(self, "_value", getattr(self, "default", None))
def _set_socket_value(self, val):
try:
self.default_value = val
return
except (AttributeError, TypeError):
pass
self._value = val
try:
setattr(bpy.types.NodeSocket, "value", property(_get_socket_value, _set_socket_value))
except Exception:
pass
class AutoBlIdName:
def __init_subclass__(cls, **kwargs):
if kwargs:
try:
super().__init_subclass__(**kwargs)
except TypeError:
super().__init_subclass__()
else:
super().__init_subclass__()
if "bl_idname" not in cls.__dict__:
cls.bl_idname = cls.__name__
if not hasattr(cls, "__annotations__"):
cls.__annotations__ = {}
for name, val in list(cls.__dict__.items()):
val_type_name = type(val).__name__
if "PropertyDeferred" in val_type_name or val_type_name.startswith("_Property") or (isinstance(val, tuple) and len(val) > 0 and "Property" in type(val[0]).__name__):
cls.__annotations__[name] = val
try:
delattr(cls, name)
except (AttributeError, TypeError):
pass
class SliceNodeSocket(NodeSocket, AutoBlIdName):
default: Any = None
bl_color = (1.0, 1.0, 1.0, 1.0)
def draw_color(self, context, node) -> tuple[float, float, float, float]: # pyright: ignore[reportIncompatibleMethodOverride]
return self.bl_color
from ...registry import get_label_by_name, register_class
class SliceNode(Node, AutoBlIdName):
_current_cache: _EvalCache | None = None
_current_visited: _VisitedSet | None = None
_data: dict[str, Any] | None = None
def get(self, key: str, default: Any = None) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
try:
return super().get(key, default) # type: ignore
except (AttributeError, TypeError):
pass
if hasattr(self, "_data") and isinstance(self._data, dict):
return self._data.get(key, default)
return getattr(self, key, default)
def __setitem__(self, key: str, value: Any) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
try:
super().__setitem__(key, value) # type: ignore
except (AttributeError, TypeError):
if not hasattr(self, "_data") or self._data is None:
self._data = {}
self._data[key] = value
def __getitem__(self, key: str) -> Any: # pyright: ignore[reportIncompatibleMethodOverride]
try:
return super().__getitem__(key) # type: ignore
except (AttributeError, TypeError):
if hasattr(self, "_data") and isinstance(self._data, dict):
return self._data[key]
raise KeyError(key)
def __contains__(self, key: str) -> bool:
try:
contains_fn = getattr(super(), "__contains__", None)
if contains_fn:
return contains_fn(key) # type: ignore
except (AttributeError, TypeError):
pass
if hasattr(self, "_data") and isinstance(self._data, dict):
return key in self._data
return False
list_socket_mapping: list[tuple[str, list[str] | str]] = []
always_list_inputs: list[str] = []
always_list_outputs: list[str] = []
is_list_output: bool = False
def update(self) -> None:
self._update_multi_groups()
self._sync_list_socket_shapes()
def _sync_list_socket_shapes(self) -> None:
from .list_utils import sync_node_list_sockets
sync_node_list_sockets(self, getattr(self, "list_socket_mapping", None))
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
def dump_inputs(self, socket_name: str = "Output") -> dict[str, Any]:
"""Captures and dumps the node's current input socket values, properties, and execution report."""
from ..debug import dump_node_execution
return dump_node_execution(self, socket_name, output_val=None, duration_s=0.0)
def resolve(self, fieldname: str) -> Any:
cache = getattr(self, "_current_cache", None)
visited = getattr(self, "_current_visited", None)
inputs = getattr(self, "inputs", None)
if inputs is None:
return resolve_input(None, cache=cache, visited=visited, fieldname=fieldname, node=self)
return resolve_input(inputs.get(fieldname), cache=cache, visited=visited, fieldname=fieldname, node=self)
def eval_multivalue(
self,
op_func: Callable[..., Any],
main_socket: str | None = None,
secondary_sockets: list[str] | None = None,
*,
socket_name: str | None = None,
default: Any = None,
is_vector: list[bool] | None = None,
is_atomic: list[bool] | None = None,
) -> Any:
from .list_utils import eval_multivalue
return eval_multivalue(
self,
op_func,
main_socket=main_socket,
secondary_sockets=secondary_sockets,
socket_name=socket_name,
default=default,
is_vector=is_vector,
is_atomic=is_atomic,
)
def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = "", description: str = ""):
bl_idname = NATIVE_SOCKET_MAP.get(bl_idname, bl_idname)
eval_name = name or NATIVE_SOCKET_LABELS.get(bl_idname) or get_label_by_name(bl_idname) or bl_idname
sock = self.outputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
sock.hide = hide
if not display_shape:
if getattr(self, "is_list_output", False) or (getattr(self, "always_list_outputs", None) and eval_name in self.always_list_outputs):
display_shape = 'LIST'
if display_shape:
sock.display_shape = display_shape
if description and hasattr(sock, "description"):
try:
sock.description = description
except Exception:
pass
return sock
def add_input(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False, display_shape: str = "", description: str = ""):
bl_idname = NATIVE_SOCKET_MAP.get(bl_idname, bl_idname)
eval_name = name or NATIVE_SOCKET_LABELS.get(bl_idname) or get_label_by_name(bl_idname) or bl_idname
sock = self.inputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
sock.hide = hide
if not display_shape:
if getattr(self, "always_list_inputs", None) and eval_name in self.always_list_inputs:
display_shape = 'LIST'
if display_shape:
sock.display_shape = display_shape
if description and hasattr(sock, "description"):
try:
sock.description = description
except Exception:
pass
return sock
def add_multi_socket_group(self, in_type: str, out_type: str = "", name: str = "", in_name: str = "", out_name: str = "", in_display_shape: str = "", out_display_shape: str = ""):
if not self.get('multi_groups'): self['multi_groups'] = {}
group_id = name or in_name or out_name or in_type
self['multi_groups'][group_id] = {
'in_type': in_type,
'out_type': out_type,
'in_name': in_name or name or get_label_by_name(in_type),
'out_name': out_name or name or (get_label_by_name(out_type) if out_type else ""),
'in_display_shape': in_display_shape,
'out_display_shape': out_display_shape,
}
self._update_multi_groups()
def resolve_multi_input(self, fieldname: str) -> Any:
cache = getattr(self, "_current_cache", None)
visited = getattr(self, "_current_visited", None)
# Search by name or identifier prefix for multi-groups
inputs = [i for i in list(self.inputs) if i.name == fieldname or i.identifier.startswith(f"{fieldname}_ms_in_")]
return [resolve_input(r, cache=cache, visited=visited, fieldname=fieldname, node=self) for r in inputs]
def _update_multi_groups(self):
if not self.get('multi_groups'): return
for group_id, cfg in self['multi_groups'].items():
in_type = cfg['in_type']
out_type = cfg.get('out_type', '')
in_name = cfg['in_name']
out_name = cfg['out_name']
in_display_shape = cfg.get('in_display_shape', '')
out_display_shape = cfg.get('out_display_shape', '')
def get_ins(): return [i for i in self.inputs if i.identifier.startswith(f"{group_id}_ms_in_")]
def get_outs(): return [o for o in self.outputs if o.identifier.startswith(f"{group_id}_ms_out_")]
def is_active(s):
if getattr(s, 'is_linked', False):
return True
val = getattr(s, 'value', None)
if val is None or val == "":
val = getattr(s, 'default_value', None)
return bool(val)
ins = get_ins()
if not ins:
self.add_input(in_type, in_name, identifier=f"{group_id}_ms_in_0", display_shape=in_display_shape)
if out_type:
self.add_output(out_type, out_name, identifier=f"{group_id}_ms_out_0", display_shape=out_display_shape)
ins = get_ins()
if is_active(ins[-1]):
idx = len(ins)
self.add_input(in_type, in_name, identifier=f"{group_id}_ms_in_{idx}", display_shape=in_display_shape)
if out_type:
self.add_output(out_type, out_name, identifier=f"{group_id}_ms_out_{idx}", display_shape=out_display_shape)
ins = get_ins()
while len(ins) > 1:
if not is_active(ins[-1]) and not is_active(ins[-2]):
self.inputs.remove(ins[-1])
outs = get_outs()
if out_type and len(outs) >= len(ins):
self.outputs.remove(outs[-1])
ins.pop()
continue
break
## Evaluation helpers
_current_execution_cache: _EvalCache | None = None
def get_node_type(node: Any) -> str:
if hasattr(node, "bl_label") and node.bl_label:
return str(node.bl_label)
if hasattr(node, "bl_idname") and node.bl_idname:
return str(node.bl_idname)
return type(node).__name__
class NodeProfiler:
def __init__(self) -> None:
# Maps node_type -> [execution_count, total_self_time_seconds]
self.stats: dict[str, list[float]] = {}
self._stack: list[dict[str, Any]] = []
@contextmanager
def profile_node(self, node: Any):
node_type = get_node_type(node)
now = time.perf_counter()
# Pause parent node's self-timer
if self._stack:
parent = self._stack[-1]
parent["self_time"] += (now - parent["start_time"])
frame = {
"node_type": node_type,
"start_time": now,
"self_time": 0.0,
}
self._stack.append(frame)
try:
yield
finally:
end_time = time.perf_counter()
popped = self._stack.pop()
total_node_self_time = popped["self_time"] + (end_time - popped["start_time"])
if node_type not in self.stats:
self.stats[node_type] = [0.0, 0.0]
self.stats[node_type][0] += 1.0
self.stats[node_type][1] += total_node_self_time
# Resume parent node's timer
if self._stack:
self._stack[-1]["start_time"] = time.perf_counter()
def format_table(self) -> str:
headers = ["node_type", "execution_count", "avg_execution_time", "tot_execution_time"]
if not self.stats:
col_widths = [20, 15, 18, 18]
header_line = " | ".join(headers[i].ljust(col_widths[i]) for i in range(4))
separator_line = "-+-".join("-" * col_widths[i] for i in range(4))
return f"{header_line}\n{separator_line}\n(No nodes executed)"
sorted_stats = sorted(self.stats.items(), key=lambda item: item[1][1], reverse=True)
rows: list[list[str]] = []
tot_count = 0
tot_time_all = 0.0
for node_type, (count, tot_time) in sorted_stats:
icount = int(count)
tot_count += icount
tot_time_all += tot_time
avg_time = tot_time / icount if icount > 0 else 0.0
rows.append([
node_type,
str(icount),
f"{avg_time:.6f}s",
f"{tot_time:.6f}s",
])
col_widths = [
max(len(headers[i]), max(len(row[i]) for row in rows))
for i in range(4)
]
col_widths[0] = max(col_widths[0], len("Total"))
col_widths[1] = max(col_widths[1], len(str(tot_count)))
col_widths[3] = max(col_widths[3], len(f"{tot_time_all:.6f}s"))
lines: list[str] = []
header_line = " | ".join(headers[i].ljust(col_widths[i]) for i in range(4))
separator_line = "-+-".join("-" * col_widths[i] for i in range(4))
lines.append(header_line)
lines.append(separator_line)
for row in rows:
lines.append(" | ".join([
row[0].ljust(col_widths[0]),
row[1].rjust(col_widths[1]),
row[2].rjust(col_widths[2]),
row[3].rjust(col_widths[3]),
]))
lines.append(separator_line)
total_row = [
"Total".ljust(col_widths[0]),
str(tot_count).rjust(col_widths[1]),
"-".rjust(col_widths[2]),
f"{tot_time_all:.6f}s".rjust(col_widths[3]),
]
lines.append(" | ".join(total_row))
return "\n".join(lines)
_current_profiler: NodeProfiler | None = None
@contextmanager
def profiler_context():
global _current_profiler
old = _current_profiler
profiler = NodeProfiler()
_current_profiler = profiler
try:
yield profiler
finally:
_current_profiler = old
@contextmanager
def profile_node_execution(node: Any):
if _current_profiler is None:
yield
return
with _current_profiler.profile_node(node):
yield
def show_in_text_editor(name: str, content: str) -> Any:
try:
text_block = bpy.data.texts.get(name)
if text_block is None:
text_block = bpy.data.texts.new(name=name)
text_block.from_string(content)
wm = getattr(bpy.context, "window_manager", None)
if wm and hasattr(wm, "windows"):
for window in wm.windows:
if hasattr(window, "screen") and window.screen and hasattr(window.screen, "areas"):
for area in window.screen.areas:
if getattr(area, "type", "") == 'TEXT_EDITOR':
for space in getattr(area, "spaces", []):
if getattr(space, "type", "") == 'TEXT_EDITOR':
space.text = text_block
return text_block
except Exception as e:
print(f"Error displaying in text editor: {e}")
return None
@contextmanager
def execution_cache():
global _current_execution_cache
if _current_execution_cache is not None:
yield _current_execution_cache
return
_current_execution_cache = {}
try:
yield _current_execution_cache
finally:
_current_execution_cache = None
@contextmanager
def _group_context(node: bpy.types.Node, cache: _EvalCache | None = None):
stack = _get_group_stack(cache)
stack.append(node)
try:
yield
finally:
stack.pop()
def _get_group_stack(cache: _EvalCache | None = None) -> list[bpy.types.Node]:
if cache is None:
cache = _current_execution_cache
if cache is None: return []
if '_group_stack' not in cache:
cache['_group_stack'] = []
return cache['_group_stack']
def _get_current_group_node(cache: _EvalCache | None = None) -> bpy.types.Node | None:
stack = _get_group_stack(cache)
return stack[-1] if stack else None
@register_class
class SLICENODE_Group(bpy.types.NodeCustomGroup, AutoBlIdName):
bl_label = "Group"
bl_icon = 'NODETREE'
def _tree_updated(self, context):
try:
if not self.name: return
_ = len(self.inputs)
_ = len(self.outputs)
except Exception:
return
self.sync_sockets()
slicer_tree: bpy.types.NodeTree | None = cast(
Any,
bpy.props.PointerProperty(
type=bpy.types.NodeTree,
name="Node Tree",
update=_tree_updated
)
)
def sync_sockets(self):
try:
if not self.name: return
_ = len(self.inputs)
_ = len(self.outputs)
except Exception:
return
if not self.slicer_tree:
self.inputs.clear()
self.outputs.clear()
return
# 1. Collect expected sockets from interface
expected_inputs = [] # (identifier, name, type)
expected_outputs = []
has_interface = hasattr(self.slicer_tree, "interface")
if has_interface:
# In Blender 4.0+, the collection of items is items_tree
items_tree = getattr(self.slicer_tree.interface, "items_tree", [])
for item in items_tree:
if getattr(item, "item_type", "") != 'SOCKET': continue
target_list = expected_inputs if item.in_out == 'INPUT' else expected_outputs
target_list.append((item.identifier, item.name, item.socket_type))
else:
# Fallback for older Blender API without interface
for sock in getattr(self.slicer_tree, "inputs", []):
expected_inputs.append((sock.identifier, sock.name, sock.bl_idname))
for sock in getattr(self.slicer_tree, "outputs", []):
expected_outputs.append((sock.identifier, sock.name, sock.bl_idname))
# 2. Sync Inputs
self._sync_socket_collection(self.inputs, expected_inputs)
# 3. Sync Outputs
self._sync_socket_collection(self.outputs, expected_outputs)
# 4. Sync Socket Shapes (LIST vs CIRCLE)
self.sync_socket_shapes()
def sync_socket_shapes(self):
try:
if not self.name or not self.slicer_tree:
return
except Exception:
return
from .list_utils import sync_group_node_socket_shapes
sync_group_node_socket_shapes(self)
def update(self):
self.sync_socket_shapes()
def _sync_socket_collection(self, collection, expected):
# Remove sockets that shouldn't be there
expected_ids = {e[0] for e in expected}
to_remove = [s for s in collection if s.identifier not in expected_ids]
for sock in to_remove:
collection.remove(sock)
# Add or update sockets
sock_indexes = {s.identifier: i for i, s in enumerate(collection)}
for ident, name, stype in expected:
stype = NATIVE_SOCKET_MAP.get(stype, stype)
if ident not in sock_indexes:
try:
# Explicitly pass identifier to keep it perfectly synced
sock = collection.new(stype, name, identifier=ident)
sock_indexes[sock.identifier] = len(collection) - 1
except Exception as e:
print(f"Failed to create socket {name} ({stype}): {e}")
else:
# Update name if changed
sock = collection[sock_indexes[ident]]
if sock.name != name:
sock.name = name
def draw_buttons(self, context, layout):
assert layout
row = layout.row(align=True)
row.prop(self, "slicer_tree", text="")
if self.slicer_tree:
op = row.operator("slicenode.group_toggle", text="", icon='NODETREE')
op.node_name = self.name
def draw_label(self):
if self.slicer_tree:
return self.slicer_tree.name
return "Group"
def _resolve_group_output(socket: NodeSocket, cache: _EvalCache | None, visited: _VisitedSet | None):
node = socket.node
assert node is not None
tree = getattr(node, "slicer_tree", None) or getattr(node, "node_tree", None)
if not tree: return None
try:
outputs_list = list(node.outputs) if node.outputs is not None else []
idx = outputs_list.index(socket)
except (ValueError, IndexError):
return None
# Find the NodeGroupOutput node that has this input linked, or the first one
output_node = None
for n in tree.nodes:
if isinstance(n, bpy.types.NodeGroupOutput):
# NodeGroupOutput inputs usually match the group outputs by index
if idx < len(n.inputs) and n.inputs[idx].is_linked:
output_node = n
break
if not output_node:
output_node = next((n for n in tree.nodes if isinstance(n, bpy.types.NodeGroupOutput)), None)
if not output_node or idx >= len(output_node.inputs): return None
inner_socket = output_node.inputs[idx]
with _group_context(node, cache=cache):
return resolve_input(typing.cast(Any, inner_socket), cache=cache, visited=visited)
def _resolve_group_input(socket: NodeSocket, cache: _EvalCache | None, visited: _VisitedSet | None):
stack = _get_group_stack(cache)
if not stack: return None
group_node = stack.pop()
# NodeGroupInput outputs usually match the group inputs by index
try:
assert socket.node is not None
outputs_list = list(socket.node.outputs) if socket.node.outputs is not None else []
idx = outputs_list.index(socket)
outer_socket = group_node.inputs[idx]
except (ValueError, IndexError):
stack.append(group_node)
return None
try:
return resolve_input(typing.cast(Any, outer_socket), cache=cache, visited=visited)
finally:
stack.append(group_node)
def _follow_reroute(current: NodeSocket) -> NodeSocket:
seen = set()
while current.node and getattr(current.node, "bl_idname", "") == "NodeReroute":
ptr = current.node.as_pointer()
if ptr in seen:
break
seen.add(ptr)
reroute_input = current.node.inputs[0] if current.node.inputs else None
if not reroute_input or not reroute_input.is_linked:
raise NodeException("Loose reroute node found: ensure all reroute nodes are correctly linked")
current = reroute_input.links[0].from_socket
return current
def _format_node_description(node: Any) -> str:
if node is None:
return ""
name = getattr(node, "name", "")
bl_label = getattr(node, "bl_label", "")
bl_idname = getattr(node, "bl_idname", "") or type(node).__name__
label = bl_label or bl_idname
if name and label and name != label:
return f"'{name}' ({label})"
elif name:
return f"'{name}'"
elif label:
return f"'{label}'"
return str(node)
def get_node_input_names(node: Any, fallback_inputs: list[str] | None = None) -> list[str]:
"""Retrieves all input socket names for a node, preserving order without duplicates."""
names: list[str] = []
inputs = getattr(node, "inputs", None)
if inputs:
for s in inputs:
s_name = getattr(s, "name", "")
if s_name and s_name not in names:
names.append(s_name)
elif not s_name:
ident = getattr(s, "identifier", "")
if ident and ident not in names:
names.append(ident)
if not names and fallback_inputs:
for s_name in fallback_inputs:
if s_name and s_name not in names:
names.append(s_name)
if not names:
mapping = getattr(node, "list_socket_mapping", None)
if mapping:
for item in mapping:
m_in = item[0] if isinstance(item, (list, tuple)) and item else None
if m_in and m_in not in names:
names.append(m_in)
return names
def format_node_failure_message(node: Any, exc_or_msg: Any, fallback_inputs: list[str] | None = None) -> str:
"""Formats a concise, informative execution failure message specifying the node and input names."""
node_str = _format_node_description(node) or "Unknown Node"
input_names = get_node_input_names(node, fallback_inputs=fallback_inputs)
inputs_str = ", ".join(input_names) if input_names else "(None)"
msg = str(exc_or_msg)
if msg.startswith("Execution failed at node "):
return msg
return f"Execution failed at node {node_str} [Inputs: {inputs_str}]: {msg}"
def attach_node_failure_info(node: Any, exc: Exception, fallback_inputs: list[str] | None = None) -> Exception:
"""Enriches an exception in-place with the failing node and input socket names, preventing duplicate wrapping."""
if getattr(exc, "_nodeslicer_node", None) is not None:
return exc
try:
setattr(exc, "_nodeslicer_node", node)
input_names = get_node_input_names(node, fallback_inputs=fallback_inputs)
setattr(exc, "_nodeslicer_inputs", input_names)
node_str = _format_node_description(node) or "Unknown Node"
inputs_str = ", ".join(input_names) if input_names else "(None)"
prefix = f"Execution failed at node {node_str} [Inputs: {inputs_str}]: "
if exc.args:
orig_msg = str(exc.args[0])
if not orig_msg.startswith("Execution failed at node "):
exc.args = (prefix + orig_msg, *exc.args[1:])
else:
exc.args = (prefix + type(exc).__name__,)
print(f"[NodeSlicer ERROR] {str(exc)}")
except Exception:
pass
return exc
class NodeErrorReportingProxy:
"""Wraps Blender operator to ensure any operator.report({'ERROR'}, ...) calls
always tell which node failed and the names of its inputs."""
def __init__(self, operator: Any, node: Any):
self._operator = operator
self._node = node
def report(self, type_set: set[str], message: str) -> None:
if self._operator and hasattr(self._operator, "report"):
if "ERROR" in type_set:
formatted_msg = format_node_failure_message(self._node, message)
print(f"[NodeSlicer ERROR] {formatted_msg}")
self._operator.report(type_set, formatted_msg)
else:
self._operator.report(type_set, message)
def __getattr__(self, name: str) -> Any:
return getattr(self._operator, name)
def _extract_caller_context() -> tuple[Any, str | None]:
try:
import sys
for depth in range(2, 6):
try:
frame = sys._getframe(depth)
except ValueError:
break
caller_self = frame.f_locals.get("self")
caller_node = None
if caller_self is not None and (
isinstance(caller_self, Node) or
hasattr(caller_self, "bl_idname") or
hasattr(caller_self, "inputs")
):
caller_node = caller_self
caller_fieldname = (
frame.f_locals.get("fieldname") or
frame.f_locals.get("socket_name") or
frame.f_locals.get("name")
)
if not isinstance(caller_fieldname, str):
caller_fieldname = None
if caller_node is not None or caller_fieldname is not None:
return caller_node, caller_fieldname
except Exception:
pass
return None, None
def object_to_mesh(data: Any, include_children: bool = True) -> Any:
"""Converts an Object (or list of Objects) into SlicingCollection, matching SLICENODE_GetMesh."""
from .blender_mesh_capture import SlicingCollection, SlicingGroup, SlicingObject
from .blender_bridge import get_all_children
mesh_types = (SlicingCollection, SlicingGroup, SlicingObject)
def _to_collection(o: Any) -> Any:
if not o or isinstance(o, mesh_types):
return o
family = [o] + (get_all_children(o) if include_children else [])
return SlicingCollection(family, getattr(o, "name", "Mesh"))
def _eval(d: Any) -> Any:
if is_list_data(d):
return [_eval(item) for item in d]
return _to_collection(d)
return _eval(data)
def _is_already_mesh(val: Any) -> bool:
if val is None:
return False
from .blender_mesh_capture import SlicingCollection, SlicingGroup, SlicingObject
mesh_types = (SlicingCollection, SlicingGroup, SlicingObject)
if isinstance(val, mesh_types):
return True
if is_list_data(val):
flat = flatten_list(val)
return bool(flat) and all(isinstance(x, mesh_types) for x in flat)
return False
def _is_object_data(val: Any) -> bool:
if val is None or _is_already_mesh(val) or isinstance(val, (str, int, float, bool, bytes, dict)):
return False
if is_list_data(val):
flat = flatten_list(val)
return bool(flat) and any(_is_object_data(x) for x in flat)
obj_type = getattr(bpy.types, "Object", None)
if isinstance(obj_type, type) and isinstance(val, obj_type):
return True
return hasattr(val, "name") or hasattr(val, "type") or hasattr(val, "matrix_world")
def resolve_input(
socket: SliceNodeSocket | None,
*,
cache: _EvalCache | None = None,
visited: _VisitedSet | None = None,
fieldname: str | None = None,
node: Any = None,
):
if socket is None:
if node is None or fieldname is None:
caller_node, caller_fieldname = _extract_caller_context()
if node is None:
node = caller_node
if fieldname is None:
fieldname = caller_fieldname
node_str = _format_node_description(node)
if fieldname and node_str:
raise NodeException(f"The queried socket '{fieldname}' on node {node_str} is non-existent")
elif fieldname:
raise NodeException(f"The queried socket '{fieldname}' is non-existent")
elif node_str:
raise NodeException(f"The queried socket on node {node_str} is non-existent")
else:
raise NodeException("The queried socket is non-existent")
assert socket.node
if cache is None:
cache = getattr(socket.node, "_current_cache", None) or _current_execution_cache
if visited is None:
visited = getattr(socket.node, "_current_visited", None)
if not socket.is_linked:
try:
val = getattr(socket, "value", None)
if val is not None and val != "": return val
except Exception:
pass
try:
val = getattr(socket, "default_value", None)
if val is not None: return val
except Exception:
pass
return getattr(socket, "default", None)
assert socket.is_linked
assert socket.links
from_socket: NodeSocket = _follow_reroute(socket.links[0].from_socket)
val = _resolve_output(
from_socket,
default=None,
cache=cache,
visited=visited,
)
is_mesh_socket = (
getattr(socket, "bl_idname", "") == "SLICESOCK_Mesh"
or type(socket).__name__ == "SLICESOCK_Mesh"
)
if is_mesh_socket and val is not None:
is_obj_source = getattr(from_socket, "bl_idname", "") in ("NodeSocketObject", "SLICESOCK_Object")
if not _is_already_mesh(val) and (is_obj_source or _is_object_data(val)):
group_stack = tuple(n.as_pointer() for n in _get_group_stack(cache))
from_node = getattr(from_socket, "node", None)
from_ptr = from_node.as_pointer() if (from_node and hasattr(from_node, "as_pointer")) else id(from_node)
from_name = getattr(from_node, "name", "") if from_node else ""
from_ident = getattr(from_socket, "identifier", getattr(from_socket, "name", ""))
conv_cache_key = ("implicit_get_mesh", group_stack, from_ptr, from_name, from_ident)
if cache is not None and conv_cache_key in cache:
return cache[conv_cache_key]
mesh_val = object_to_mesh(val, include_children=True)
if cache is not None:
cache[conv_cache_key] = mesh_val
return mesh_val
return val
def _resolve_output(
socket: NodeSocket,
*,
default: Any = None,
cache: _EvalCache | None = None,
visited: _VisitedSet | None = None,
):
if cache is None:
cache = _current_execution_cache
cache = {} if cache is None else cache
visited = set() if visited is None else visited
assert socket.node
# Include group stack in cache key to ensure independent evaluation for multiple instances
group_stack = tuple(n.as_pointer() for n in _get_group_stack(cache))
# Use identifier instead of name for the cache key, as multiple sockets
# (especially in multi-groups) can share the same name but must have unique identifiers.
node_ptr = socket.node.as_pointer() if hasattr(socket.node, "as_pointer") else id(socket.node)
node_name = getattr(socket.node, "name", "")
key = (group_stack, node_ptr, node_name, socket.identifier)
if key in cache: return cache[key]
node_key = (group_stack, node_ptr, node_name)
if node_key in visited: return default # cycle guard
visited.add(node_key)
try:
with execution_cache():
with profile_node_execution(socket.node):
if not isinstance(socket.node, SliceNode):
is_group = isinstance(socket.node, SLICENODE_Group) or (
isinstance(getattr(bpy.types, "NodeGroup", None), type) and isinstance(socket.node, bpy.types.NodeGroup)
) or getattr(socket.node, "bl_idname", "") in ("NodeGroup", "GeometryNodeGroup")
if is_group:
return _resolve_group_output(socket, cache=cache, visited=visited)
is_group_input = (
isinstance(getattr(bpy.types, "NodeGroupInput", None), type) and isinstance(socket.node, bpy.types.NodeGroupInput)
) or getattr(socket.node, "bl_idname", "") == "NodeGroupInput"
if is_group_input:
return _resolve_group_input(socket, cache=cache, visited=visited)
return default
# Set these temporarily so self.resolve() can find them
socket.node._current_cache = cache # type: ignore
socket.node._current_visited = visited # type: ignore
try:
if getattr(socket.node, "mute", False):
# Attempt bypass: find a matching input socket by name or type
input_to_bypass = socket.node.inputs.get(socket.name)
if not input_to_bypass:
for inp in socket.node.inputs:
if inp.bl_idname == socket.bl_idname:
input_to_bypass = inp
break
if input_to_bypass:
value = resolve_input(typing.cast(Any, input_to_bypass), cache=cache, visited=visited)
cache[key] = value
return value
return default
assert socket.node.evaluate_output
start_eval_time = time.perf_counter()
eval_exc = None
eval_tb = None
value = None
try:
value = socket.node.evaluate_output(
socket.name,
identifier=socket.identifier,
socket=socket,
cache=cache,
visited=visited
)
cache[key] = value
return value
except Exception as e:
eval_exc = e
import traceback
eval_tb = traceback.format_exc()
attach_node_failure_info(socket.node, e)
raise
finally:
try:
from ..debug import _GLOBAL_DEBUG_ENABLED, dump_node_execution
if _GLOBAL_DEBUG_ENABLED or getattr(socket.node, "_debug_dump", False):
dur = time.perf_counter() - start_eval_time
dump_node_execution(
socket.node,
socket.name,
output_val=value if eval_exc is None else None,
duration_s=dur,
exception=eval_exc,
tb_str=eval_tb,
log_console=True
)
except Exception:
pass
finally:
socket.node._current_cache = None # type: ignore
socket.node._current_visited = None # type: ignore
finally:
visited.remove(node_key)