This commit is contained in:
Nicolas Predella 2026-10-14 13:40:15 +03:00
parent e540c85e59
commit 2f09772928
5 changed files with 256 additions and 404 deletions

View file

@ -32,12 +32,6 @@ class SLICENODE_PathsToGcode(SliceNode):
layout.prop(self, "use_header")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
paths: Any = self.resolve("Paths")
intrinsics: Any = self.resolve("Intrinsics")
if not paths:
return ""
def _paths_to_gcode(p: Any, intr: Any) -> str:
if not p:
return ""
@ -59,11 +53,12 @@ class SLICENODE_PathsToGcode(SliceNode):
return pather.to_gcode(include_header=self.use_header)
return eval_element_wise_multivalue(
return self.eval_multivalue(
_paths_to_gcode,
paths,
intrinsics,
input_names=["Intrinsics"]
main_socket="Paths",
secondary_sockets=["Intrinsics"],
socket_name=socket_name,
default="",
)
@register_class
@ -88,10 +83,6 @@ class SLICENODE_AssignMetadata(SliceNode):
self.add_output("NodeSocketString", "Gcode")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
name = self.resolve("Name")
filament = self.resolve("Filament Type")
printer = self.resolve("Printer Model")
def _meta_one(n: Any, f: Any, p: Any) -> str:
lines = []
if n:
@ -102,12 +93,12 @@ class SLICENODE_AssignMetadata(SliceNode):
lines.append(f'; printer_model = {p}')
return '\n'.join(lines)
return eval_element_wise_multivalue(
return self.eval_multivalue(
_meta_one,
name,
filament,
printer,
input_names=["Filament Type", "Printer Model"]
main_socket="Name",
secondary_sockets=["Filament Type", "Printer Model"],
socket_name=socket_name,
default="",
)
@register_class
@ -123,15 +114,6 @@ class SLICENODE_GcodeRegexReplace(SliceNode):
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
import re
gcode = self.resolve("Gcode")
find = self.resolve("Find") or ""
replace = self.resolve("Replace") or ""
if not gcode:
return ""
if not find:
return gcode
def _replace_one(gc: Any, f_pattern: Any, r_pattern: Any) -> str:
gc_str = str(gc or "")
@ -153,12 +135,12 @@ class SLICENODE_GcodeRegexReplace(SliceNode):
print(f"Regex error in node {self.name}: {e}")
return gc_str
return eval_element_wise_multivalue(
return self.eval_multivalue(
_replace_one,
gcode,
find,
replace,
input_names=["Find", "Replace"]
main_socket="Gcode",
secondary_sockets=["Find", "Replace"],
socket_name=socket_name,
default="",
)
@register_class

View file

@ -129,12 +129,6 @@ class SLICENODE_SeparateMesh(SliceNode):
layout.prop(self, "comparison", text="")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
coll: Any = self.resolve("Mesh")
attr_name: Any = self.resolve("Attribute")
value: Any = self.resolve("Value")
if not coll: return None
def _split_coll(sc: SlicingCollection, a_name: str, comp_val: float) -> tuple[SlicingCollection, SlicingCollection]:
res_selection = SlicingCollection([], sc.name + "_selection")
res_inverted = SlicingCollection([], sc.name + "_inverted")
@ -191,29 +185,13 @@ class SLICENODE_SeparateMesh(SliceNode):
res_inverted.objects.append(so_inv)
return res_selection, res_inverted
is_list, items, (attr_list, val_list) = prepare_indexed_inputs(
coll,
attr_name,
value,
input_names=["Attribute", "Value"]
return self.eval_multivalue(
lambda sc, a, v: _split_coll(sc, str(a or ""), float(v if v is not None else 0.0)),
main_socket="Mesh",
secondary_sockets=["Attribute", "Value"],
socket_name=socket_name,
)
if is_list:
results = [
_split_coll(c, str(a or ""), float(v if v is not None else 0.0))
for c, a, v in zip(items, attr_list, val_list)
]
return [r[0] for r in results] if socket_name == "Selection" else [r[1] for r in results]
res_selection, res_inverted = _split_coll(
items[0],
str(attr_list[0] or ""),
float(val_list[0] if val_list[0] is not None else 0.0)
)
if socket_name == "Selection":
return res_selection
return res_inverted
@register_class
class SLICENODE_SeparateMeshByRange(SliceNode):
bl_label = "Separate Mesh by Range"
@ -268,12 +246,6 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
col.prop(self, "end_val")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
coll: Any = self.resolve("Mesh")
attr_name: str = self.resolve("Attribute")
if not coll: return None
# Check if we can parse the socket name as an integer
try:
target_val = int(socket_name)
except ValueError:
@ -317,7 +289,7 @@ class SLICENODE_SeparateMeshByRange(SliceNode):
res.objects.append(so_new)
return res
return eval_element_wise_multivalue(_split_coll, coll, attr_name, input_names=["Attribute"])
return self.eval_multivalue(_split_coll, "Mesh", ["Attribute"])
@register_class
class SLICENODE_ArrangeMesh(SliceNode):
@ -583,37 +555,17 @@ class SLICENODE_TransformGeometry(SliceNode):
self.add_output("SLICESOCK_Geometry", "Geometry")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name not in ("Geometry",):
return None
geom_data: Any = self.resolve("Geometry")
if geom_data is None:
return None
translation = self.resolve("Translation")
if translation is None:
translation = (0.0, 0.0, 0.0)
rotation = self.resolve("Rotation")
if rotation is None:
rotation = (0.0, 0.0, 0.0)
scale = self.resolve("Scale")
if scale is None:
scale = (1.0, 1.0, 1.0)
def _transform_one(g: Any, t: Any, r: Any, s: Any) -> Any:
t_arr = np.array(t, dtype=np.float64)
r_arr = np.array(r, dtype=np.float64)
s_arr = np.array(s, dtype=np.float64)
t_arr = np.array(t if t is not None else (0.0, 0.0, 0.0), dtype=np.float64)
r_arr = np.array(r if r is not None else (0.0, 0.0, 0.0), dtype=np.float64)
s_arr = np.array(s if s is not None else (1.0, 1.0, 1.0), dtype=np.float64)
return _transform_geometry_item(g, t_arr, r_arr, s_arr)
return eval_element_wise_multivalue(
return self.eval_multivalue(
_transform_one,
geom_data,
translation,
rotation,
scale,
is_vector=[True, True, True],
input_names=["Translation", "Rotation", "Scale"]
main_socket="Geometry",
secondary_sockets=["Translation", "Rotation", "Scale"],
socket_name=socket_name,
)
@ -629,33 +581,15 @@ class SLICENODE_GetTransforms(SliceNode):
self.add_output("NodeSocketVector", "Scale")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
geom_data: Any = self.resolve("Geometry")
if geom_data is None:
if socket_name == "Scale":
return (1.0, 1.0, 1.0)
return (0.0, 0.0, 0.0)
from ....core.slice import extract_geometry_transforms
from ....infra.blender.list_utils import is_list_data
if is_list_data(geom_data):
results = [extract_geometry_transforms(g) for g in geom_data]
if socket_name == "Translation":
return [r[0] for r in results]
elif socket_name == "Rotation":
return [r[1] for r in results]
elif socket_name == "Scale":
return [r[2] for r in results]
return (0.0, 0.0, 0.0)
t, r, s = extract_geometry_transforms(geom_data)
if socket_name == "Translation":
return t
elif socket_name == "Rotation":
return r
elif socket_name == "Scale":
return s
return (0.0, 0.0, 0.0)
default = (1.0, 1.0, 1.0) if socket_name == "Scale" else (0.0, 0.0, 0.0)
return self.eval_multivalue(
extract_geometry_transforms,
main_socket="Geometry",
secondary_sockets=[],
socket_name=socket_name,
default=default,
)
@register_class
@ -670,19 +604,12 @@ class SLICENODE_DropGeometry(SliceNode):
self.add_output("NodeSocketVector", "Translation")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
mesh_data: Any = self.resolve("Mesh")
if not mesh_data:
return None
z = self.resolve('Z')
if z is None:
z = 0.0
def _drop_item(item: Any, target_z: float) -> tuple[Any, tuple[float, float, float]]:
def _drop_item(item: Any, target_z: Any) -> tuple[Any, tuple[float, float, float]]:
if not item or getattr(item, "min_z", None) is None:
return item, (0.0, 0.0, 0.0)
z_float = float(target_z if target_z is not None else 0.0)
min_z = round(item.min_z, 6)
offset_z = float(target_z - min_z)
offset_z = float(z_float - min_z)
if isinstance(item, SlicingGroup):
copied_data = _copy_slicing_group(item)
else:
@ -700,23 +627,14 @@ class SLICENODE_DropGeometry(SliceNode):
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
return copied_data, (0.0, 0.0, offset_z)
is_list, items, (z_vals,) = prepare_indexed_inputs(
mesh_data,
z,
input_names=["Z"]
return self.eval_multivalue(
_drop_item,
main_socket="Mesh",
secondary_sockets=["Z"],
socket_name=socket_name,
default=(0.0, 0.0, 0.0) if socket_name == "Translation" else None
)
if is_list:
results = [_drop_item(m, float(z_i if z_i is not None else 0.0)) for m, z_i in zip(items, z_vals)]
if socket_name == "Translation":
return [r[1] for r in results]
return [r[0] for r in results]
dropped, trans = _drop_item(items[0], float(z_vals[0] if z_vals[0] is not None else 0.0))
if socket_name == "Translation":
return trans
return dropped
def format_name_template(template: str, so: Any, default_old_name: str = "") -> str:
if not template or "{" not in template or "}" not in template:
@ -789,25 +707,11 @@ class SLICENODE_RenameMesh(SliceNode):
self.add_output("SLICESOCK_Mesh", "Geometry")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Geometry":
return None
geom: Any = self.resolve("Geometry")
if geom is None:
return None
target_name = self.resolve("Name")
def _rename_one(g: Any, name: Any) -> Any:
if not name:
return g
return _rename_slicing_item(g, str(name))
return eval_element_wise_multivalue(
_rename_one,
geom,
target_name,
input_names=["Name"]
return self.eval_multivalue(
lambda g, name: _rename_slicing_item(g, str(name)) if name else g,
main_socket="Geometry",
secondary_sockets=["Name"],
socket_name=socket_name
)
@ -865,15 +769,6 @@ class SLICENODE_AssignPdfProperties(SliceNode):
layout.prop(self, "show_name")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Geometry":
return None
geom: Any = self.resolve("Geometry")
if not geom:
return None
width_socket = self.resolve("Line Width")
lw = width_socket if width_socket is not None else float(self.line_width)
lc = (float(self.line_color[0]), float(self.line_color[1]), float(self.line_color[2]))
sn = bool(self.show_name)
@ -881,12 +776,7 @@ class SLICENODE_AssignPdfProperties(SliceNode):
w = float(line_w) if line_w is not None else float(self.line_width)
return _assign_pdf_properties_to_item(g, w, lc, sn)
return eval_element_wise_multivalue(
_assign_one,
geom,
lw,
input_names=["Line Width"]
)
return self.eval_multivalue(_assign_one, "Geometry", ["Line Width"], socket_name=socket_name)
def _parse_settings_props(raw_settings: Any) -> list[tuple[str, str]]:
@ -956,28 +846,9 @@ class SLICENODE_AssignPropertyToObject(SliceNode):
self.add_output("SLICESOCK_Mesh", "Geometry")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Geometry":
return None
geom: Any = self.resolve("Geometry")
if not geom:
return None
raw_settings = self.resolve("Settings")
if not _is_single_settings_spec(raw_settings):
settings_arg = list(raw_settings)
else:
settings_arg = raw_settings
def _assign_one(g: Any, s: Any) -> Any:
props = _parse_settings_props(s)
return _assign_properties_to_item(g, props)
return eval_element_wise_multivalue(
_assign_one,
geom,
settings_arg,
input_names=["Settings"]
)
return self.eval_multivalue(_assign_one, "Geometry", ["Settings"], socket_name=socket_name)

View file

@ -30,11 +30,6 @@ class SLICENODE_GetPaths(SliceNode):
pass
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
obj: Any = self.resolve("Object")
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
def _get_single_path(o: Any, e_name: Any, f_name: Any, extra: Any) -> SlicePaths:
@ -50,13 +45,12 @@ class SLICENODE_GetPaths(SliceNode):
sp.objects_info = {"objects": [{"name": name_val, "polygon": []}]}
return sp
return eval_element_wise_multivalue(
return self.eval_multivalue(
_get_single_path,
obj,
e_attr_name,
f_attr_name,
extra_attrs,
input_names=["Extrusion Multiplier Attribute", "Feedrate Attribute", "Extra Attributes"]
main_socket="Object",
secondary_sockets=["Extrusion Multiplier Attribute", "Feedrate Attribute", "Extra Attributes"],
socket_name=socket_name,
default=SlicePaths(),
)
def _slice_single_mesh_item(coll: Any, layer_height: float, fl_height: float | None, rotation: Any, ignore_holes: bool) -> SlicePaths:
@ -193,25 +187,6 @@ class SLICENODE_SliceShells(SliceNode):
layout.prop(self, "ignore_holes")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
coll: Any = self.resolve("Mesh")
layer_height: float = float(self.resolve("Layer Height") or 0.2)
if layer_height <= 0:
layer_height = 0.2
fl_height: float | None = None
try:
fl_val = self.resolve("First Layer Height")
if fl_val is not None:
fl_height = float(fl_val)
except Exception:
fl_height = None
if fl_height is not None and fl_height <= 0:
fl_height = None
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
ignore_holes: bool = bool(getattr(self, "ignore_holes", False))
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
try:
@ -221,21 +196,17 @@ class SLICENODE_SliceShells(SliceNode):
except Exception:
pass
if not coll: return SlicePaths()
def _slice_one(item: Any, lh_val: Any, flh_val: Any, rot_val: Any) -> SlicePaths:
lh = float(lh_val) if lh_val is not None and float(lh_val) > 0 else 0.2
flh = float(flh_val) if flh_val is not None and float(flh_val) > 0 else None
return _slice_single_mesh_item(item, lh, flh, rot_val, ignore_holes)
return eval_element_wise_multivalue(
return self.eval_multivalue(
_slice_one,
coll,
layer_height,
fl_height,
rotation,
is_vector=[False, False, True],
input_names=["Layer Height", "First Layer Height", "Rotation"]
main_socket="Mesh",
secondary_sockets=["Layer Height", "First Layer Height", "Rotation"],
socket_name=socket_name,
default=SlicePaths(),
)
@register_class
@ -263,26 +234,16 @@ class SLICENODE_BufferShells(SliceNode):
layout.prop(self, "shape_option")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
paths = self.resolve("Paths")
distance_val = self.resolve("Distance")
distance: float = float(distance_val) if distance_val is not None else 0.0
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
if not paths: return SlicePaths()
def _buf(p: Any, dist: Any, rot: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.buffer(float(dist if dist is not None else 0.0), miter=self.shape_option, rotation=rot)
return eval_element_wise_multivalue(
return self.eval_multivalue(
_buf,
paths,
distance,
rotation,
is_vector=[False, True],
input_names=["Distance", "Rotation"]
main_socket="Paths",
secondary_sockets=["Distance", "Rotation"],
socket_name=socket_name,
default=SlicePaths(),
)
@register_class
@ -329,14 +290,6 @@ class SLICENODE_BufferCurve(SliceNode):
layout.prop(self, "single_sided")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
paths = self.resolve("Paths")
distance = self.resolve("Distance")
rotation = self.resolve("Rotation")
if not paths: return SlicePaths()
def _buf(p: Any, dist: Any, rot: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.buffer_curve(
@ -347,13 +300,12 @@ class SLICENODE_BufferCurve(SliceNode):
rotation=rot
)
return eval_element_wise_multivalue(
return self.eval_multivalue(
_buf,
paths,
distance,
rotation,
is_vector=[False, True],
input_names=["Distance", "Rotation"]
main_socket="Paths",
secondary_sockets=["Distance", "Rotation"],
socket_name=socket_name,
default=SlicePaths(),
)
@register_class
@ -382,14 +334,6 @@ class SLICENODE_OffsetCurve(SliceNode):
layout.prop(self, "join_style")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
paths = self.resolve("Paths")
distance = self.resolve("Distance")
rotation = self.resolve("Rotation")
if not paths: return SlicePaths()
def _off(p: Any, dist: Any, rot: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.offset_curve(
@ -398,13 +342,12 @@ class SLICENODE_OffsetCurve(SliceNode):
rotation=rot
)
return eval_element_wise_multivalue(
return self.eval_multivalue(
_off,
paths,
distance,
rotation,
is_vector=[False, True],
input_names=["Distance", "Rotation"]
main_socket="Paths",
secondary_sockets=["Distance", "Rotation"],
socket_name=socket_name,
default=SlicePaths(),
)
@register_class
@ -418,21 +361,16 @@ class SLICENODE_PathOffset(SliceNode):
self.add_output("SLICESOCK_Paths", "Paths")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
paths = self.resolve("Paths")
offset = self.resolve("Offset")
if not paths: return SlicePaths()
def _off(p: Any, off: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.offset(np.array(off if off is not None else (0.0, 0.0, 0.0), dtype=np.float64))
return eval_element_wise_multivalue(
return self.eval_multivalue(
_off,
paths,
offset,
is_vector=[True],
input_names=["Offset"]
main_socket="Paths",
secondary_sockets=["Offset"],
socket_name=socket_name,
default=SlicePaths(),
)
@register_class
@ -447,24 +385,17 @@ class SLICENODE_RotatePath(SliceNode):
self.add_output("SLICESOCK_Paths", "Paths")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
paths = self.resolve("Paths")
angle = self.resolve("Angle")
center = self.resolve("Center")
if not paths: return SlicePaths()
def _rot(p: Any, ang: Any, cent: Any) -> SlicePaths:
if not p: return SlicePaths()
center_vec = np.array(cent, dtype=np.float64) if cent and any(cent) else None
return p.rotate(float(ang if ang is not None else 0.0), center=center_vec)
return eval_element_wise_multivalue(
return self.eval_multivalue(
_rot,
paths,
angle,
center,
is_vector=[False, True],
input_names=["Angle", "Center"]
main_socket="Paths",
secondary_sockets=["Angle", "Center"],
socket_name=socket_name,
default=SlicePaths(),
)
@register_class
@ -496,15 +427,6 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
layout.prop(self, "shape_option")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
paths = self.resolve("Paths")
base_distance = self.resolve("Distance")
width_step = self.resolve("Width Step")
count = self.resolve("Count")
rotation = self.resolve("Rotation")
if not paths:
return SlicePaths()
def _buf_multi(p: Any, b_dist: Any, w_step: Any, cnt: Any, rot: Any) -> SlicePaths:
c = int(cnt if cnt is not None else 1)
if not p or c <= 0:
@ -521,15 +443,12 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
return p.buffer_multiple(dist, c, step, miter=self.shape_option, rotation=rot)
return SlicePaths()
return eval_element_wise_multivalue(
return self.eval_multivalue(
_buf_multi,
paths,
base_distance,
width_step,
count,
rotation,
is_vector=[False, False, False, True],
input_names=["Distance", "Width Step", "Count", "Rotation"]
main_socket="Paths",
secondary_sockets=["Distance", "Width Step", "Count", "Rotation"],
socket_name=socket_name,
default=SlicePaths(),
)
@register_class
@ -560,15 +479,6 @@ class SLICENODE_Infill(SliceNode):
layout.prop(self, "infill_type", text="")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
paths = self.resolve("Paths")
density = self.resolve("Density")
infill_rotation = self.resolve("Infill Rotation")
rotation = self.resolve("Layer Rotation")
if not paths: return SlicePaths()
def _inf(p: Any, dens: Any, inf_rot: Any, rot: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.infill(
@ -578,14 +488,12 @@ class SLICENODE_Infill(SliceNode):
layer_rotation=rot
)
return eval_element_wise_multivalue(
return self.eval_multivalue(
_inf,
paths,
density,
infill_rotation,
rotation,
is_vector=[False, False, True],
input_names=["Density", "Infill Rotation", "Layer Rotation"]
main_socket="Paths",
secondary_sockets=["Density", "Infill Rotation", "Layer Rotation"],
socket_name=socket_name,
default=SlicePaths(),
)
@register_class
@ -602,18 +510,17 @@ class SLICENODE_OverhangMask(SliceNode):
pass
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
paths = self.resolve("Paths")
tolerance = self.resolve("Tolerance")
if not paths: return SlicePaths()
def _mask(p: Any, tol: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.get_overhang_mask(tolerance=float(tol if tol is not None else 0.05))
return eval_element_wise_multivalue(_mask, paths, tolerance, input_names=["Tolerance"])
return self.eval_multivalue(
_mask,
main_socket="Paths",
secondary_sockets=["Tolerance"],
socket_name=socket_name,
default=SlicePaths(),
)
@register_class
class SLICENODE_PathOperations(SliceNodeJoiner):
@ -752,21 +659,19 @@ class SLICENODE_SimplifyPaths(SliceNode):
layout.prop(self, "preserve_topology")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths":
return SlicePaths()
paths = self.resolve("Paths")
tolerance = self.resolve("Tolerance")
if not paths:
return SlicePaths()
def _simp(p: Any, tol: Any) -> SlicePaths:
t_val = float(tol if tol is not None else 0.0)
if not p or t_val <= 0:
return p if isinstance(p, SlicePaths) else SlicePaths()
return p.simplify(t_val, preserve_topology=self.preserve_topology)
return eval_element_wise_multivalue(_simp, paths, tolerance, input_names=["Tolerance"])
return self.eval_multivalue(
_simp,
main_socket="Paths",
secondary_sockets=["Tolerance"],
socket_name=socket_name,
default=SlicePaths(),
)
@register_class
@ -804,17 +709,6 @@ class SLICENODE_SeamPlacement(SliceNode):
layout.prop(self, "acquire_transforms")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths":
return SlicePaths()
paths = self.resolve("Paths")
mesh_input: Any = self.resolve("Mesh")
stagger_val = self.resolve("Seam Staggering")
stagger_mm: float = float(stagger_val) if stagger_val is not None else 0.0
if not paths:
return SlicePaths()
acquire_t = bool(getattr(self, "acquire_transforms", True))
def _place(p: Any, m_in: Any, stg: Any) -> SlicePaths:
@ -825,12 +719,12 @@ class SLICENODE_SeamPlacement(SliceNode):
return p
return p.place_seams(m_in, mode=self.mode.lower(), acquire_transforms=acquire_t, stagger_mm=s_val)
return eval_element_wise_multivalue(
return self.eval_multivalue(
_place,
paths,
mesh_input,
stagger_mm,
input_names=["Mesh", "Seam Staggering"]
main_socket="Paths",
secondary_sockets=["Mesh", "Seam Staggering"],
socket_name=socket_name,
default=SlicePaths(),
)

View file

@ -145,6 +145,27 @@ class SliceNode(Node, AutoBlIdName):
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,
) -> 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,
)
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

View file

@ -85,47 +85,31 @@ def prepare_indexed_inputs(
(is_main_list, main_items, broadcasted_secondary_lists)
"""
main_is_list = is_input_a_list(main_data, is_vector=False)
if main_is_list:
main_items = list(main_data)
n = len(main_items)
else:
main_items = [main_data]
n = 1
main_items = list(main_data) if main_is_list else [main_data]
n = len(main_items)
vec_flags = is_vector or [False] * len(secondary_inputs)
names = input_names or [f"input_{i+1}" for i in range(len(secondary_inputs))]
broadcasted_secondaries: list[list[Any]] = []
for i, sec_val in enumerate(secondary_inputs):
is_vec = vec_flags[i] if i < len(vec_flags) else False
name = names[i] if i < len(names) else f"input_{i+1}"
sec_is_list = is_input_a_list(sec_val, is_vector=is_vec)
if not sec_is_list:
if main_is_list:
broadcasted_secondaries.append([sec_val] * n)
else:
broadcasted_secondaries.append([sec_val])
if not is_input_a_list(sec_val, is_vector=is_vec):
broadcasted_secondaries.append([sec_val] * n)
elif len(sec_val) == n:
broadcasted_secondaries.append(list(sec_val))
elif len(sec_val) == 1:
broadcasted_secondaries.append([sec_val[0]] * n)
else:
m = len(sec_val)
if not main_is_list:
if m == 1:
broadcasted_secondaries.append([sec_val[0]])
else:
raise ValueError(
f"Input list length mismatch: main input is a single item, but secondary input '{name}' has {m} items (must be 1 or a single value)."
)
else:
if m == n:
broadcasted_secondaries.append(list(sec_val))
elif m == 1:
broadcasted_secondaries.append([sec_val[0]] * n)
else:
raise ValueError(
f"Input list length mismatch: main input has {n} items, but secondary input '{name}' has {m} items (must be {n} or 1)."
)
expected = "1 or a single value" if not main_is_list else f"{n} or 1"
main_desc = f"{n} items" if main_is_list else "a single item"
raise ValueError(
f"Input list length mismatch: main input has {main_desc}, "
f"but secondary input '{name}' has {m} items (must be {expected})."
)
return main_is_list, main_items, broadcasted_secondaries
@ -135,6 +119,7 @@ def eval_element_wise_multivalue(
*secondary_inputs: Any,
is_vector: list[bool] | None = None,
input_names: list[str] | None = None,
output_index: int | None = None,
) -> Any:
"""Evaluates op_func element-wise with support for main and secondary list inputs:
- If main_data is a list, returns a list of results.
@ -142,6 +127,7 @@ def eval_element_wise_multivalue(
- Secondary inputs that are lists are matched index per index.
- Secondary inputs that are single values (or length-1 lists) are broadcast across all main elements.
- Mismatched secondary list lengths raise ValueError.
- If output_index is not None, unpacks result[output_index] from the op_func return value.
"""
is_main_list, main_items, sec_lists = prepare_indexed_inputs(
main_data,
@ -150,12 +136,110 @@ def eval_element_wise_multivalue(
input_names=input_names,
)
def _call(item: Any, i: int) -> Any:
res = op_func(item, *(sec[i] for sec in sec_lists))
if output_index is not None and isinstance(res, tuple):
try:
return res[output_index]
except (IndexError, TypeError):
return None
return res
if is_main_list:
return [
op_func(item, *(sec[i] for sec in sec_lists))
for i, item in enumerate(main_items)
]
return op_func(main_items[0], *(sec[0] for sec in sec_lists))
return [_call(item, i) for i, item in enumerate(main_items)]
return _call(main_items[0], 0)
def is_socket_vector_type(node: Any, socket_name: str) -> bool:
"""Checks whether the socket on the node is a vector/rotation/color socket."""
inputs = getattr(node, "inputs", None)
if inputs:
sock = None
if hasattr(inputs, "get"):
sock = inputs.get(socket_name)
elif isinstance(inputs, (list, tuple)):
for s in inputs:
if getattr(s, "name", "") == socket_name:
sock = s
break
if sock is not None:
bl_id = getattr(sock, "bl_idname", "")
if bl_id in ("NodeSocketVector", "NodeSocketRotation", "NodeSocketColor", "NodeSocketVectorXYZ", "NodeSocketVectorEuler"):
return True
if bl_id in ("NodeSocketFloat", "NodeSocketInt", "NodeSocketString", "NodeSocketBool", "SLICESOCK_Mesh", "SLICESOCK_Paths", "SLICESOCK_Settings"):
return False
name_lower = socket_name.lower()
return name_lower in ("translation", "rotation", "scale", "offset", "center", "layer rotation", "color")
def eval_multivalue(
node: Any,
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,
) -> Any:
"""Generalized element-wise multi-value evaluator for SliceNode instances."""
mapping = getattr(node, "list_socket_mapping", None)
# Infer main_socket if not specified
if main_socket is None:
if mapping and len(mapping) > 0 and mapping[0][0]:
main_socket = mapping[0][0]
else:
inputs = getattr(node, "inputs", None)
if inputs and len(inputs) > 0:
main_socket = getattr(inputs[0], "name", "")
else:
return default
# Determine output index / socket validation
output_index: int | None = None
if socket_name is not None and mapping:
out_spec = next((outs for m_in, outs in mapping if m_in == main_socket), None)
if out_spec is not None:
if isinstance(out_spec, (list, tuple)):
if socket_name not in out_spec:
return default
output_index = out_spec.index(socket_name)
elif isinstance(out_spec, str) and out_spec:
if socket_name != out_spec:
return default
# Resolve main input
resolve_fn = getattr(node, "resolve", None)
if not callable(resolve_fn):
return default
main_data = resolve_fn(main_socket)
if main_data is None:
return default
if isinstance(main_data, list) and len(main_data) == 0:
return []
# Infer secondary_sockets if not specified
if secondary_sockets is None:
inputs = getattr(node, "inputs", [])
secondary_sockets = [s.name for s in inputs if getattr(s, "name", "") != main_socket]
# Resolve secondary inputs
sec_vals = [resolve_fn(sname) for sname in secondary_sockets]
# Infer is_vector if not specified
if is_vector is None:
vec_flags = [is_socket_vector_type(node, sname) for sname in secondary_sockets]
else:
vec_flags = is_vector
return eval_element_wise_multivalue(
op_func,
main_data,
*sec_vals,
is_vector=vec_flags,
input_names=secondary_sockets,
output_index=output_index,
)
def eval_element_wise(op_func: Callable[[Any], Any], data: Any) -> Any:
"""If data is a list, applies op_func to each element and returns a list.