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

View file

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

View file

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

View file

@ -85,46 +85,30 @@ def prepare_indexed_inputs(
(is_main_list, main_items, broadcasted_secondary_lists) (is_main_list, main_items, broadcasted_secondary_lists)
""" """
main_is_list = is_input_a_list(main_data, is_vector=False) main_is_list = is_input_a_list(main_data, is_vector=False)
if main_is_list: main_items = list(main_data) if main_is_list else [main_data]
main_items = list(main_data)
n = len(main_items) n = len(main_items)
else:
main_items = [main_data]
n = 1
vec_flags = is_vector or [False] * len(secondary_inputs) vec_flags = is_vector or [False] * len(secondary_inputs)
names = input_names or [f"input_{i+1}" for i in range(len(secondary_inputs))] names = input_names or [f"input_{i+1}" for i in range(len(secondary_inputs))]
broadcasted_secondaries: list[list[Any]] = [] broadcasted_secondaries: list[list[Any]] = []
for i, sec_val in enumerate(secondary_inputs): for i, sec_val in enumerate(secondary_inputs):
is_vec = vec_flags[i] if i < len(vec_flags) else False is_vec = vec_flags[i] if i < len(vec_flags) else False
name = names[i] if i < len(names) else f"input_{i+1}" 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 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) broadcasted_secondaries.append([sec_val] * n)
else: elif len(sec_val) == n:
broadcasted_secondaries.append([sec_val])
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)) broadcasted_secondaries.append(list(sec_val))
elif m == 1: elif len(sec_val) == 1:
broadcasted_secondaries.append([sec_val[0]] * n) broadcasted_secondaries.append([sec_val[0]] * n)
else: else:
m = len(sec_val)
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( raise ValueError(
f"Input list length mismatch: main input has {n} items, but secondary input '{name}' has {m} items (must be {n} or 1)." 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 return main_is_list, main_items, broadcasted_secondaries
@ -135,6 +119,7 @@ def eval_element_wise_multivalue(
*secondary_inputs: Any, *secondary_inputs: Any,
is_vector: list[bool] | None = None, is_vector: list[bool] | None = None,
input_names: list[str] | None = None, input_names: list[str] | None = None,
output_index: int | None = None,
) -> Any: ) -> Any:
"""Evaluates op_func element-wise with support for main and secondary list inputs: """Evaluates op_func element-wise with support for main and secondary list inputs:
- If main_data is a list, returns a list of results. - 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 lists are matched index per index.
- Secondary inputs that are single values (or length-1 lists) are broadcast across all main elements. - Secondary inputs that are single values (or length-1 lists) are broadcast across all main elements.
- Mismatched secondary list lengths raise ValueError. - 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( is_main_list, main_items, sec_lists = prepare_indexed_inputs(
main_data, main_data,
@ -150,12 +136,110 @@ def eval_element_wise_multivalue(
input_names=input_names, 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: if is_main_list:
return [ return [_call(item, i) for i, item in enumerate(main_items)]
op_func(item, *(sec[i] for sec in sec_lists)) return _call(main_items[0], 0)
for i, item in enumerate(main_items)
] def is_socket_vector_type(node: Any, socket_name: str) -> bool:
return op_func(main_items[0], *(sec[0] for sec in sec_lists)) """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: 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. """If data is a list, applies op_func to each element and returns a list.