Refactor multi-socket group system and fix G-code generation metadata synchronization
This commit is contained in:
parent
5f49097f80
commit
5aad206c48
5 changed files with 168 additions and 118 deletions
|
|
@ -357,12 +357,12 @@ class SLICENODE_GetPaths(SliceNode):
|
|||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||
obj: Any = self.resolve("Object")
|
||||
attr_name: str = self.resolve("Extrusion Multiplier Attribute")
|
||||
e_attr_name: str = self.resolve("Extrusion Multiplier Attribute")
|
||||
f_attr_name: str = self.resolve("Feedrate Attribute")
|
||||
extra_attrs: dict[str, str] = self.resolve("Extra Attributes")
|
||||
|
||||
from ...infra.blender.blender_mesh_capture import capture_mesh_edges
|
||||
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(obj, attr_name=attr_name, f_attr_name=f_attr_name, extra_attrs=extra_attrs)
|
||||
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(obj, e_attr_name, f_attr_name, extra_attrs)
|
||||
|
||||
return SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
||||
|
||||
|
|
@ -723,7 +723,7 @@ class SliceNodeJoiner(SliceNode):
|
|||
|
||||
def init(self, context):
|
||||
self.add_output(self.output_socket_type or self.socket_type)
|
||||
self.add_multi_input(self.socket_type)
|
||||
self.add_multi_socket_group(self.socket_type)
|
||||
|
||||
@register_class
|
||||
class SLICENODE_JoinGcode(SliceNodeJoiner):
|
||||
|
|
@ -997,7 +997,7 @@ class SLICENODE_GcodeToFile(ExecSliceNode):
|
|||
def init(self, context):
|
||||
self._init_exec()
|
||||
self.add_input("SLICESOCK_DirPath", "Folder")
|
||||
self.add_multi_input("SLICESOCK_Gcode", "Gcode")
|
||||
self.add_multi_socket_group("SLICESOCK_Gcode", name="Gcode")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
super().draw_buttons(context, layout)
|
||||
|
|
@ -1156,7 +1156,7 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
|||
|
||||
def init(self, context):
|
||||
self._init_exec()
|
||||
self.add_multi_input("SLICESOCK_Gcode", "Gcode")
|
||||
self.add_multi_socket_group("SLICESOCK_Gcode", name="Gcode")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
super().draw_buttons(context, layout)
|
||||
|
|
@ -1176,7 +1176,6 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
|||
import json
|
||||
|
||||
if not gc:
|
||||
if operator: operator.report({'ERROR'}, "No Gcode to send")
|
||||
continue
|
||||
|
||||
# Parse Metadata from Gcode
|
||||
|
|
@ -1250,10 +1249,8 @@ class ExecSliceNode_DatabaseSubmit(ExecSliceNode):
|
|||
|
||||
record_id = self._submit_to_db(token, fields, temp_path)
|
||||
if operator: operator.report({'INFO'}, f"Submitted to {self._db_name} (Record: {record_id})")
|
||||
return 'ok'
|
||||
except Exception as e:
|
||||
if operator: operator.report({'ERROR'}, f"{self._db_name} Error: {str(e)}")
|
||||
return 'error'
|
||||
finally:
|
||||
if temp_path.exists():
|
||||
temp_path.unlink()
|
||||
|
|
@ -1344,37 +1341,39 @@ class SLICENODE_PrusaslicerSlice(SliceNode):
|
|||
bl_label = "PrusaSlicer Slice"
|
||||
|
||||
prusaslicer_executable: NfStringProperty(name="PrusaSlicer Path", subtype='FILE_PATH', default='flatpak run com.prusa3d.PrusaSlicer') # pyright: ignore[reportInvalidTypeForm]
|
||||
gcode_result: NfStringProperty(name="Gcode Result", default="") # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
def init(self, context):
|
||||
self.add_input("SLICESOCK_Mesh", "Mesh")
|
||||
self.add_input("SLICESOCK_PrusaslicerConf", "Config")
|
||||
self.add_input("SLICESOCK_Settings", "Settings")
|
||||
self.add_output("SLICESOCK_GcodeRo")
|
||||
self.add_multi_socket_group("SLICESOCK_Mesh", "SLICESOCK_GcodeRo", name="Slicing Group")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
assert layout
|
||||
layout.prop(self, "prusaslicer_executable")
|
||||
|
||||
def evaluate_output(self, socket_name: str, **kwargs) -> Any:
|
||||
if socket_name == "Gcode":
|
||||
self.evaluate()
|
||||
return self.gcode_result
|
||||
def evaluate_output(self, socket_name: str, identifier: str = "", **kwargs) -> Any:
|
||||
if identifier.startswith("Slicing Group_ms_out_"):
|
||||
try:
|
||||
idx = int(identifier.split("_")[-1])
|
||||
return self.evaluate_for_index(idx)
|
||||
except (ValueError, IndexError):
|
||||
return ""
|
||||
return super().evaluate_output(socket_name, **kwargs)
|
||||
|
||||
def evaluate(self, operator=None):
|
||||
self.gcode_result = ""
|
||||
mesh_data: SlicingCollection | SlicingGroup = self.resolve("Mesh")
|
||||
def evaluate_for_index(self, index: int, operator=None) -> str:
|
||||
mesh_groups: list[SlicingCollection | SlicingGroup] = self.resolve_multi_input("Slicing Group")
|
||||
if index >= len(mesh_groups):
|
||||
return ""
|
||||
|
||||
mesh_data = mesh_groups[index]
|
||||
if not mesh_data:
|
||||
return ""
|
||||
|
||||
conf_data = self.resolve("Config")
|
||||
overrides = self.resolve("Settings")
|
||||
|
||||
if not mesh_data:
|
||||
if operator: operator.report({'ERROR'}, "No Mesh data connected")
|
||||
return 'no object'
|
||||
if not conf_data:
|
||||
if operator: operator.report({'ERROR'}, "No Config connected")
|
||||
return 'no config'
|
||||
return ""
|
||||
|
||||
# Extract data from config dict
|
||||
printer_id = conf_data['printer']
|
||||
|
|
@ -1409,7 +1408,6 @@ class SLICENODE_PrusaslicerSlice(SliceNode):
|
|||
slicing_group.offset(offset)
|
||||
|
||||
# Prepare overrides for generate_conf_writer
|
||||
# The format expected by generate_conf_writer is overrides={'key': {'value': 'val'}}
|
||||
writer_overrides = {k: {'value': v} for k, v in overrides.items()}
|
||||
|
||||
# Generate the ConfigWriter
|
||||
|
|
@ -1426,72 +1424,49 @@ class SLICENODE_PrusaslicerSlice(SliceNode):
|
|||
|
||||
# Prepare 3mf path
|
||||
temp_dir = Path(tempfile.gettempdir())
|
||||
output_path = temp_dir / f"tmp.3mf"
|
||||
output_path = temp_dir / f"tmp_{index}.3mf"
|
||||
|
||||
try:
|
||||
from ...infra._3mf_writer._3mf import prepare_3mf
|
||||
prepare_3mf(output_path, slicing_group, conf_writer, z_gcodes=[])
|
||||
|
||||
# Actual slicing
|
||||
exe_str = self.prusaslicer_executable
|
||||
if not exe_str:
|
||||
if operator: operator.report({'ERROR'}, "PrusaSlicer executable path is empty")
|
||||
return 'error'
|
||||
if not exe_str: return ""
|
||||
|
||||
# Parse command (handles multi-word commands like flatpak)
|
||||
cmd_parts = shlex.split(exe_str)
|
||||
if not cmd_parts:
|
||||
if operator: operator.report({'ERROR'}, "Invalid PrusaSlicer command")
|
||||
return 'error'
|
||||
if not cmd_parts: return ""
|
||||
|
||||
# Try to resolve the first part as a path if it's not a command in PATH
|
||||
if not shutil.which(cmd_parts[0]):
|
||||
abs_path = bpy.path.abspath(cmd_parts[0])
|
||||
if os.path.exists(abs_path):
|
||||
cmd_parts[0] = abs_path
|
||||
|
||||
# Final check if command exists
|
||||
if not shutil.which(cmd_parts[0]):
|
||||
if operator: operator.report({'ERROR'}, f"Command or executable not found: {cmd_parts[0]}")
|
||||
self.gcode_result = ""
|
||||
return 'error'
|
||||
return ""
|
||||
|
||||
gcode_path = output_path.with_suffix(".gcode")
|
||||
|
||||
# Command: <exe> --export-gcode <3mf_path> --output <gcode_path>
|
||||
cmd = cmd_parts + ["--export-gcode", str(output_path), "--output", str(gcode_path)]
|
||||
|
||||
# Run PrusaSlicer
|
||||
process = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if process.returncode != 0:
|
||||
if operator: operator.report({'ERROR'}, f"PrusaSlicer failed: {process.stderr}")
|
||||
self.gcode_result = ""
|
||||
return 'error'
|
||||
return ""
|
||||
|
||||
# Read G-code
|
||||
if gcode_path.exists():
|
||||
with open(gcode_path, 'r', encoding='utf-8') as f:
|
||||
self.gcode_result = f.read()
|
||||
self.gcode_result += f"\n; binary_gcode = {binary_gcode_value}\n"
|
||||
gcode_result = f.read()
|
||||
gcode_result += f"\n; binary_gcode = {binary_gcode_value}\n"
|
||||
|
||||
output_path.unlink()
|
||||
if gcode_path.exists(): gcode_path.unlink()
|
||||
|
||||
if operator:
|
||||
operator.report({'INFO'}, f"Slicing complete. G-code generated.")
|
||||
return gcode_result
|
||||
else:
|
||||
if operator: operator.report({'ERROR'}, "G-code file was not generated by PrusaSlicer")
|
||||
self.gcode_result = ""
|
||||
return 'error'
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
self.gcode_result = ""
|
||||
if operator: operator.report({'ERROR'}, f"Failed to slice: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 'error'
|
||||
|
||||
return 'ok'
|
||||
|
||||
return ""
|
||||
@register_class
|
||||
class SLICENODE_PrusaslicerPreview(ExecSliceNode):
|
||||
bl_label = "PrusaSlicer Preview"
|
||||
|
|
@ -1595,7 +1570,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
last_e = 0.0
|
||||
|
||||
offset_x, offset_y = 0.0, 0.0
|
||||
default_extrusion_rate = 0.017 # Default fallback
|
||||
custom_default_extrusion_rate = 0.0
|
||||
|
||||
last_v_idx = -1
|
||||
last_p2 = None
|
||||
|
|
@ -1614,7 +1589,7 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
try: offset_y = float(comment.split('=')[1])
|
||||
except ValueError: pass
|
||||
elif comment.startswith("DEFAULT_EXTRUSION_RATE="):
|
||||
try: default_extrusion_rate = float(comment.split('=')[1])
|
||||
try: custom_default_extrusion_rate = float(comment.split('=')[1])
|
||||
except ValueError: pass
|
||||
|
||||
line_no_comment = line.split(';')[0].strip()
|
||||
|
|
@ -1651,8 +1626,8 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
|||
|
||||
# Calculate e_rate (proportion between E and default_extrusion_rate)
|
||||
seg_len_mm = segment_length_3d(p1, p2) * 1000.0
|
||||
if seg_len_mm > 0 and default_extrusion_rate > 0:
|
||||
e_rate = (new_e / seg_len_mm) / default_extrusion_rate
|
||||
if seg_len_mm > 0 and custom_default_extrusion_rate > 0:
|
||||
e_rate = (new_e / seg_len_mm) / custom_default_extrusion_rate
|
||||
else:
|
||||
e_rate = 1.0
|
||||
|
||||
|
|
@ -1744,7 +1719,7 @@ class SLICENODE_Executor(ExecSliceNode, SliceNodeJoiner):
|
|||
|
||||
def init(self, context):
|
||||
self._init_exec()
|
||||
self.add_multi_input(self.socket_type, "Links")
|
||||
self.add_multi_socket_group(self.socket_type, name="Links")
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
super().draw_buttons(context, layout)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
import bpy
|
||||
from bpy.props import CollectionProperty
|
||||
from bpy.types import Property, PropertyGroup, bpy_struct
|
||||
from .bpy_ns_types import NfStringProperty, NfFloatProperty, NfIntProperty, NfEnumProperty, NfPointerProperty, NfFloatVectorProperty
|
||||
|
||||
from ...registry import register_class
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
from __future__ import annotations
|
||||
from numpy import float64
|
||||
import numpy as np
|
||||
import pyclipper
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from .geometry import ndarray_lru_cache
|
||||
|
||||
|
|
@ -111,13 +112,42 @@ class SlicePaths(list[np.ndarray]):
|
|||
if isinstance(other, SlicePaths):
|
||||
self.extend(other)
|
||||
self.groups.extend(other.groups)
|
||||
self.extrusion_rate.extend(other.extrusion_rate)
|
||||
self.feedrate.extend(other.feedrate)
|
||||
for k, v in other.extra_attributes.items():
|
||||
if k in self.extra_attributes:
|
||||
self.extra_attributes[k].extend(v)
|
||||
else:
|
||||
# Pad with zeros for existing paths if we didn't have this attribute
|
||||
self.extra_attributes[k] = [np.zeros(len(p)) for p in self[:-len(other)]] + list(v)
|
||||
# Pad existing attributes if 'other' didn't have them
|
||||
for k in self.extra_attributes:
|
||||
if k not in other.extra_attributes:
|
||||
self.extra_attributes[k].extend([np.zeros(len(p)) for p in other])
|
||||
else:
|
||||
self.extend(other)
|
||||
self.groups.extend(list(range(0,len(other))))
|
||||
self.extrusion_rate.extend([np.ones(len(p)) for p in other])
|
||||
self.feedrate.extend([np.zeros(len(p)) for p in other])
|
||||
for k in self.extra_attributes:
|
||||
self.extra_attributes[k].extend([np.zeros(len(p)) for p in other])
|
||||
|
||||
def append_with_group(self, path: np.ndarray, group: int = 0):
|
||||
def append_with_group(self, path: np.ndarray, group: int = 0, extrusion_rate: np.ndarray | None = None, feedrate: np.ndarray | None = None, extra_attributes: dict[str, np.ndarray] | None = None):
|
||||
self.append(path)
|
||||
self.groups.append(group)
|
||||
self.extrusion_rate.append(extrusion_rate if extrusion_rate is not None else np.ones(len(path)))
|
||||
self.feedrate.append(feedrate if feedrate is not None else np.zeros(len(path)))
|
||||
|
||||
extra_attributes = extra_attributes or {}
|
||||
for k, v in extra_attributes.items():
|
||||
if k in self.extra_attributes:
|
||||
self.extra_attributes[k].append(v)
|
||||
else:
|
||||
self.extra_attributes[k] = [np.zeros(len(p)) for p in self[:-1]] + [v]
|
||||
|
||||
for k in self.extra_attributes:
|
||||
if k not in extra_attributes:
|
||||
self.extra_attributes[k].append(np.zeros(len(path)))
|
||||
|
||||
def slice_at_range(models, z_values) -> list[list[np.ndarray]]:
|
||||
if not models:
|
||||
|
|
@ -539,9 +569,26 @@ class Pather(object):
|
|||
|
||||
def __init__(self, paths: list[np.ndarray], **kwargs) -> None:
|
||||
self.paths = paths
|
||||
self.extrusion_rates = getattr(paths, "extrusion_rate", [np.ones(len(p)) for p in paths])
|
||||
self.feedrates = getattr(paths, "feedrate", [np.zeros(len(p)) for p in paths])
|
||||
self.extra_attributes = getattr(paths, "extra_attributes", {})
|
||||
|
||||
# Robust initialization: ensure metadata lists match paths length
|
||||
e_rate = getattr(paths, "extrusion_rate", None)
|
||||
if e_rate is not None and len(e_rate) == len(paths):
|
||||
self.extrusion_rates = e_rate
|
||||
else:
|
||||
self.extrusion_rates = [np.ones(len(p)) for p in paths]
|
||||
|
||||
f_rate = getattr(paths, "feedrate", None)
|
||||
if f_rate is not None and len(f_rate) == len(paths):
|
||||
self.feedrates = f_rate
|
||||
else:
|
||||
self.feedrates = [np.zeros(len(p)) for p in paths]
|
||||
|
||||
extra = getattr(paths, "extra_attributes", None)
|
||||
if extra is not None and all(len(v) == len(paths) for v in extra.values()):
|
||||
self.extra_attributes = extra
|
||||
else:
|
||||
self.extra_attributes = {}
|
||||
|
||||
self.bed_center_x: float = 250.
|
||||
self.bed_center_y: float = 250.
|
||||
self.feedrate: float = 1200.
|
||||
|
|
@ -599,8 +646,8 @@ class Pather(object):
|
|||
if len(coords) < 2:
|
||||
continue
|
||||
|
||||
e_rates = self.extrusion_rates[i]
|
||||
f_rates = self.feedrates[i]
|
||||
e_rate = self.extrusion_rates[i]
|
||||
f_rate = self.feedrates[i]
|
||||
extras = {prefix: vals[i] for prefix, vals in self.extra_attributes.items()}
|
||||
|
||||
x0, y0, z0 = coords[0]
|
||||
|
|
@ -618,10 +665,10 @@ class Pather(object):
|
|||
old_extras = {prefix: None for prefix in extras}
|
||||
for j, (p1, p2) in enumerate(zip(coords, coords[1:])):
|
||||
seg_len: float = segment_length_3d(p1, p2)
|
||||
e_segment = seg_len * self.extrusion_rate * e_rates[j]
|
||||
e_segment = seg_len * self.extrusion_rate * e_rate[j]
|
||||
|
||||
# Per-segment feedrate value (not multiplier)
|
||||
fr = f_rates[j] if f_rates[j] > 0 else self.feedrate
|
||||
fr = f_rate[j] if f_rate[j] > 0 else self.feedrate
|
||||
|
||||
old_x, old_y, old_z = p1
|
||||
x, y, z = p2
|
||||
|
|
|
|||
|
|
@ -410,7 +410,7 @@ def _cached_capture_mesh_edges(
|
|||
mesh_name: str,
|
||||
matrix_tuple: tuple[float, ...],
|
||||
scene_scale: float,
|
||||
attr_name: str = "e_mult",
|
||||
e_attr_name: str = "e_mult",
|
||||
f_attr_name: str = "f_rate",
|
||||
extra_attrs: dict[str, str] | None = None
|
||||
) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
||||
|
|
@ -438,8 +438,8 @@ def _cached_capture_mesh_edges(
|
|||
mesh.vertices.foreach_get("co", verts.ravel())
|
||||
|
||||
e_mults = np.ones(num_verts, dtype=np.float64)
|
||||
if attr_name in mesh.attributes:
|
||||
mesh.attributes[attr_name].data.foreach_get("value", e_mults)
|
||||
if e_attr_name in mesh.attributes:
|
||||
mesh.attributes[e_attr_name].data.foreach_get("value", e_mults)
|
||||
|
||||
f_rates = np.zeros(num_verts, dtype=np.float64)
|
||||
if f_attr_name in mesh.attributes:
|
||||
|
|
@ -496,7 +496,7 @@ def _cached_capture_mesh_edges(
|
|||
if hasattr(obj_eval, "to_mesh_clear"):
|
||||
obj_eval.to_mesh_clear()
|
||||
|
||||
def capture_mesh_edges(obj: Object, attr_name: str = "e_mult", f_attr_name: str = "f_rate", extra_attrs: dict[str, str] | None = None) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
||||
def capture_mesh_edges(obj: Object, e_attr_name: str = "e_mult", f_attr_name: str = "f_rate", extra_attrs: dict[str, str] | None = None) -> tuple[list[NDArray[float64]], list[NDArray[float64]], list[NDArray[float64]], dict[str, list[NDArray[float64]]]]:
|
||||
if not obj or not obj.data: return [], [], [], {}
|
||||
|
||||
if obj.mode == 'EDIT':
|
||||
|
|
@ -513,7 +513,7 @@ def capture_mesh_edges(obj: Object, attr_name: str = "e_mult", f_attr_name: str
|
|||
obj.data.name,
|
||||
matrix_tuple,
|
||||
scene_scale,
|
||||
attr_name,
|
||||
e_attr_name,
|
||||
f_attr_name,
|
||||
extra_attrs
|
||||
)
|
||||
|
|
@ -27,7 +27,7 @@ class SliceNodeSocket(NodeSocket, AutoBlIdName):
|
|||
from ...registry import get_label_by_name
|
||||
class SliceNode(Node, AutoBlIdName):
|
||||
def update(self) -> None:
|
||||
self._update_multi_inputs()
|
||||
self._update_multi_groups()
|
||||
|
||||
def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ...
|
||||
|
||||
|
|
@ -36,50 +36,71 @@ class SliceNode(Node, AutoBlIdName):
|
|||
visited = getattr(self, "_current_visited", None)
|
||||
return resolve_input(self.inputs.get(fieldname), cache=cache, visited=visited)
|
||||
|
||||
def add_output(self, bl_idname: str, name: str = "", identifier: str = ""):
|
||||
def add_output(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False):
|
||||
eval_name = name or get_label_by_name(bl_idname)
|
||||
return self.outputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
|
||||
sock = self.outputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
|
||||
sock.hide = hide
|
||||
return sock
|
||||
|
||||
def add_input(self, bl_idname: str, name: str = "", identifier: str = ""):
|
||||
def add_input(self, bl_idname: str, name: str = "", identifier: str = "", hide: bool = False):
|
||||
eval_name = name or get_label_by_name(bl_idname)
|
||||
return self.inputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
|
||||
sock = self.inputs.new(bl_idname, eval_name, identifier=identifier or eval_name)
|
||||
sock.hide = hide
|
||||
return sock
|
||||
|
||||
def add_multi_input(self, bl_idname: str, name: str = "") -> None:
|
||||
if not self.get('multi_sockets'): self['multi_sockets'] = {}
|
||||
if not self['multi_sockets'].get(name): self['multi_sockets'][name] = {'bl_idname': bl_idname, 'name': name or get_label_by_name(bl_idname)}
|
||||
self._update_multi_inputs()
|
||||
def add_multi_socket_group(self, in_type: str, out_type: str = "", name: str = "", in_name: str = "", out_name: 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 ""),
|
||||
}
|
||||
self._update_multi_groups()
|
||||
|
||||
def resolve_multi_input(self, fieldname) -> Any:
|
||||
cache = getattr(self, "_current_cache", None)
|
||||
visited = getattr(self, "_current_visited", None)
|
||||
inputs = [i for i in list(self.inputs) if i.name == fieldname]
|
||||
# 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) for r in inputs]
|
||||
|
||||
def _update_multi_inputs(self):
|
||||
if not self.get('multi_sockets'): return
|
||||
for k, ms in self['multi_sockets'].items():
|
||||
def is_active(input) -> bool:
|
||||
a: bool = input.is_linked or getattr(input, 'value', False)
|
||||
b: bool = getattr(inputs()[-1], 'value', False)
|
||||
return a or b
|
||||
def _update_multi_groups(self):
|
||||
if not self.get('multi_groups'): return
|
||||
|
||||
def inputs():
|
||||
return [i for i in list(self.inputs) if i.name == ms['name']]
|
||||
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']
|
||||
|
||||
socket_type = ms['bl_idname']
|
||||
name = ms['name']
|
||||
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): return s.is_linked or bool(getattr(s, 'value', False))
|
||||
|
||||
if len(inputs()) == 0:
|
||||
self.add_input(socket_type, name, name+str(len(inputs())))
|
||||
return
|
||||
ins = get_ins()
|
||||
|
||||
if is_active(inputs()[-1]):
|
||||
self.add_input(socket_type, name, name+str(len(inputs())))
|
||||
if not ins:
|
||||
self.add_input(in_type, in_name, identifier=f"{group_id}_ms_in_0")
|
||||
if out_type:
|
||||
self.add_output(out_type, out_name, identifier=f"{group_id}_ms_out_0")
|
||||
ins = get_ins()
|
||||
|
||||
while True:
|
||||
if len(inputs()) < 2: break
|
||||
if not is_active(inputs()[-1]) and not is_active(inputs()[-2]):
|
||||
self.inputs.remove(inputs()[-1])
|
||||
if is_active(ins[-1]):
|
||||
idx = len(ins)
|
||||
self.add_input(in_type, in_name, identifier=f"{group_id}_ms_in_{idx}")
|
||||
if out_type:
|
||||
self.add_output(out_type, out_name, identifier=f"{group_id}_ms_out_{idx}")
|
||||
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
|
||||
|
||||
|
|
@ -201,7 +222,9 @@ def _resolve_output(
|
|||
visited = set() if visited is None else visited
|
||||
|
||||
assert socket.node
|
||||
key = (socket.node.as_pointer(), socket.node.name, socket.name)
|
||||
# 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.
|
||||
key = (socket.node.as_pointer(), socket.node.name, socket.identifier)
|
||||
if key in cache: return cache[key]
|
||||
|
||||
node_key = (socket.node.as_pointer(), socket.node.name)
|
||||
|
|
@ -234,7 +257,13 @@ def _resolve_output(
|
|||
return default
|
||||
|
||||
assert socket.node.evaluate_output
|
||||
value = socket.node.evaluate_output(socket.name, cache=cache, visited=visited)
|
||||
value = socket.node.evaluate_output(
|
||||
socket.name,
|
||||
identifier=socket.identifier,
|
||||
socket=socket,
|
||||
cache=cache,
|
||||
visited=visited
|
||||
)
|
||||
cache[key] = value
|
||||
return value
|
||||
finally:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue