From 2f09772928d8f825a0e3015d4ce1af0c749b9db3 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Wed, 14 Oct 2026 13:40:15 +0300 Subject: [PATCH] refac --- core/nodeslicer/nodes/gcode.py | 48 ++-- core/nodeslicer/nodes/mesh.py | 203 +++------------- core/nodeslicer/nodes/path.py | 234 +++++-------------- infra/blender/blender_slice_nodes_generic.py | 21 ++ infra/blender/list_utils.py | 154 +++++++++--- 5 files changed, 256 insertions(+), 404 deletions(-) diff --git a/core/nodeslicer/nodes/gcode.py b/core/nodeslicer/nodes/gcode.py index ed5f63c..e0465f3 100644 --- a/core/nodeslicer/nodes/gcode.py +++ b/core/nodeslicer/nodes/gcode.py @@ -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 diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index 4b0c996..caeb6f4 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -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) diff --git a/core/nodeslicer/nodes/path.py b/core/nodeslicer/nodes/path.py index 28c3e0e..d2aa4ce 100644 --- a/core/nodeslicer/nodes/path.py +++ b/core/nodeslicer/nodes/path.py @@ -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(), ) diff --git a/infra/blender/blender_slice_nodes_generic.py b/infra/blender/blender_slice_nodes_generic.py index 43fd5a2..a21dcde 100644 --- a/infra/blender/blender_slice_nodes_generic.py +++ b/infra/blender/blender_slice_nodes_generic.py @@ -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 diff --git a/infra/blender/list_utils.py b/infra/blender/list_utils.py index ce725ad..1fca24f 100644 --- a/infra/blender/list_utils.py +++ b/infra/blender/list_utils.py @@ -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.