import copy from typing import Any import numpy as np from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfIntProperty, NfFloatProperty, NfStringProperty, NfFloatVectorProperty, notify_tree_changed from ....registry import register_class from .base import SliceNode, SlicingCollection, get_all_children, SliceNodeJoiner, SliceNodeListJoiner, SlicingGroup, SlicingObject, SlicePaths from ....infra.blender.blender_mesh_capture import PdfProperties from ....core.pdf_exporter.geometry import transform_slice_paths from ....core.geometry import update_transform from ....infra.blender.list_utils import eval_element_wise, eval_element_wise_multivalue, prepare_indexed_inputs, flatten_list, is_list_data, is_input_a_list @register_class class SLICENODE_GetMesh(SliceNode): bl_label = "Get Mesh" list_socket_mapping = [("Object", "Mesh")] include_children: bool = NfBoolProperty(name="Include Children", default=True) def init(self, context): self.add_input("NodeSocketObject") self.add_output("SLICESOCK_Mesh") def draw_buttons(self, context, layout): assert layout layout.prop(self, "include_children") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: if socket_name != "Mesh": return None obj: Any = self.resolve("Object") if not obj: return None from ....infra.blender.blender_slice_nodes_generic import object_to_mesh return object_to_mesh(obj, include_children=self.include_children) @register_class class SLICENODE_MeshesToList(SliceNodeListJoiner): bl_label = "Meshes to List" socket_type = "SLICESOCK_Mesh" socket_label = "Mesh" output_socket_name = "Meshes" @register_class class SLICENODE_JoinMesh(SliceNodeJoiner): bl_label = "Join Mesh Parts" socket_type = "SLICESOCK_Mesh" def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection: from ....infra.blender.blender_slice_nodes_generic import resolve_input all_colls = [] for inp in self.inputs: if getattr(inp, "is_linked", False) or getattr(inp, "value", None): val = resolve_input(inp) if val: all_colls.extend(flatten_list(val)) first_coll = all_colls[0] if all_colls else None merged_coll = SlicingCollection([], first_coll.name if first_coll else '') for coll in all_colls: if hasattr(coll, "objects"): merged_coll.objects += list(coll.objects) elif isinstance(coll, SlicingObject): merged_coll.objects.append(coll) for t in getattr(coll, "transforms", []): if t not in merged_coll.transforms: merged_coll.transforms.append(dict(t)) return merged_coll @register_class class SLICENODE_JoinMeshGroups(SliceNodeJoiner): bl_label = "Join Mesh Groups" socket_type = "SLICESOCK_Mesh" is_list_output = True always_list_outputs = ["Mesh"] def init(self, context): self.add_output(self.output_socket_type or self.socket_type, display_shape='LIST') self.add_multi_socket_group(self.socket_type) def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup: from ....infra.blender.blender_slice_nodes_generic import resolve_input group: SlicingGroup = SlicingGroup([]) all_colls = [] for input_socket in self.inputs: if getattr(input_socket, "is_linked", False) or getattr(input_socket, "value", None): val = resolve_input(input_socket) if val: all_colls.extend(flatten_list(val)) for idx, coll in enumerate(all_colls): col_name = getattr(coll, "name", "") if not col_name and getattr(coll, "objects", None): col_name = coll.objects[0].name if not col_name: col_name = f"collection_{idx}" group.collections[col_name] = coll return group @register_class class SLICENODE_SeparateMesh(SliceNode): bl_label = "Separate Mesh" list_socket_mapping = [("Mesh", ["Selection", "Inverted"])] comparison: str = NfEnumProperty( name="Comparison", items=[ ('EQUAL', "Equal", "=="), ('NOT_EQUAL', "Not Equal", "!="), ('GREATER_THAN', "Greater Than", ">"), ('LESS_THAN', "Less Than", "<"), ('GREATER_EQUAL', "Greater Equal", ">="), ('LESS_EQUAL', "Less Equal", "<="), ], default='EQUAL', update=notify_tree_changed ) def init(self, context): self.add_input("SLICESOCK_Mesh") self.add_input("NodeSocketString", "Attribute") self.add_input("NodeSocketFloat", "Value") self.add_output("SLICESOCK_Mesh", "Selection") self.add_output("SLICESOCK_Mesh", "Inverted") def draw_buttons(self, context, layout): assert layout layout.prop(self, "comparison", text="") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: 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") for so in sc.objects: if not a_name or a_name not in so.attributes: res_inverted.objects.append(so) continue attr = so.attributes[a_name] vals = attr["values"] mask = np.zeros(len(so.mesh), dtype=bool) if self.comparison == 'EQUAL': mask = vals == comp_val elif self.comparison == 'NOT_EQUAL': mask = vals != comp_val elif self.comparison == 'GREATER_THAN': mask = vals > comp_val elif self.comparison == 'LESS_THAN': mask = vals < comp_val elif self.comparison == 'GREATER_EQUAL': mask = vals >= comp_val elif self.comparison == 'LESS_EQUAL': mask = vals <= comp_val if np.all(mask): res_selection.objects.append(so) elif not np.any(mask): res_inverted.objects.append(so) else: so_sel = SlicingObject() so_sel.name = so.name + "_sel" so_sel.parent = so.parent so_sel.mesh = so.mesh[mask] so_sel.transforms = [dict(t) for t in getattr(so, "transforms", [])] so_sel.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() so_sel.pdf_object_properties = list(getattr(so, "pdf_object_properties", [])) for k, v in so.attributes.items(): so_sel.attributes[k] = { "domain": v["domain"], "type": v["type"], "values": v["values"][mask] } res_selection.objects.append(so_sel) so_inv = SlicingObject() so_inv.name = so.name + "_inv" so_inv.parent = so.parent so_inv.mesh = so.mesh[~mask] so_inv.transforms = [dict(t) for t in getattr(so, "transforms", [])] so_inv.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() so_inv.pdf_object_properties = list(getattr(so, "pdf_object_properties", [])) for k, v in so.attributes.items(): so_inv.attributes[k] = { "domain": v["domain"], "type": v["type"], "values": v["values"][~mask] } res_inverted.objects.append(so_inv) return res_selection, res_inverted 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, ) @register_class class SLICENODE_SeparateMeshByRange(SliceNode): bl_label = "Separate Mesh by Range" start_val: int = NfIntProperty(name="Start", default=0, update=notify_tree_changed) end_val: int = NfIntProperty(name="End", default=1, update=notify_tree_changed) def init(self, context): self.add_input("SLICESOCK_Mesh") self.add_input("NodeSocketString", "Attribute") self._update_outputs() def update(self): super().update() self._update_outputs() def _update_outputs(self): # Ensure end >= start start = int(self.start_val) if isinstance(getattr(self, "start_val", None), (int, float)) else 0 end_val = int(self.end_val) if isinstance(getattr(self, "end_val", None), (int, float)) else 1 end = max(start, end_val) expected_ids = [str(i) for i in range(start, end + 1)] from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape inp = self.inputs.get("Mesh") if hasattr(self, "inputs") else None is_list = is_socket_linked_to_list(inp) if inp is not None else False if inp is not None: set_socket_shape(inp, "LIST" if is_list else "CIRCLE") attr_inp = self.inputs.get("Attribute") if hasattr(self, "inputs") else None if attr_inp is not None: set_socket_shape(attr_inp, "LIST" if is_socket_linked_to_list(attr_inp) else "CIRCLE") # Remove outputs that are no longer in the range to_remove = [o for o in self.outputs if o.identifier not in expected_ids] for o in to_remove: self.outputs.remove(o) # Add new outputs for id_str in expected_ids: if id_str not in self.outputs: self.add_output("SLICESOCK_Mesh", id_str, identifier=id_str, display_shape="LIST" if is_list else "CIRCLE") else: set_socket_shape(self.outputs[id_str], "LIST" if is_list else "CIRCLE") # Re-order to match range for i, id_str in enumerate(expected_ids): current_index = self.outputs.find(id_str) if current_index != -1 and current_index != i: self.outputs.move(current_index, i) def draw_buttons(self, context, layout): assert layout col = layout.column(align=True) col.prop(self, "start_val") col.prop(self, "end_val") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: try: target_val = int(socket_name) except ValueError: return None start = int(self.start_val) if isinstance(getattr(self, "start_val", None), (int, float)) else 0 end_val = int(self.end_val) if isinstance(getattr(self, "end_val", None), (int, float)) else 1 end = max(start, end_val) total_parts = end - start + 1 num_digits = max(2, len(str(total_parts))) i = target_val - start def _split_coll(sc: SlicingCollection, a_name: str) -> SlicingCollection: res = SlicingCollection([], f"{sc.name}_{target_val}") for so in sc.objects: if not a_name or a_name not in so.attributes: continue attr = so.attributes[a_name] vals = attr["values"] # Attribute values might be floats or ints, we check for equality mask = vals == target_val if np.any(mask): # Split the object so_new = SlicingObject() base_mesh_name = so.name so_new.name = f"{base_mesh_name}{i:0{num_digits}d}" so_new.parent = so.parent so_new.mesh = so.mesh[mask] so_new.transforms = [dict(t) for t in getattr(so, "transforms", [])] so_new.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() so_new.pdf_object_properties = list(getattr(so, "pdf_object_properties", [])) for k, v in so.attributes.items(): so_new.attributes[k] = { "domain": v["domain"], "type": v["type"], "values": v["values"][mask] } res.objects.append(so_new) return res return self.eval_multivalue(_split_coll, "Mesh", ["Attribute"]) @register_class class SLICENODE_ArrangeMesh(SliceNode): bl_label = "Arrange Slicing Group" list_socket_mapping = [("Slicing Group", "Slicing Group")] method: str = NfEnumProperty( name="Method", items=[ ('NFP', "NFP (Default)", "Nest using No-Fit Polygon placer + First-Fit selection"), ('BL', "Bottom-Left", "Nest using Bottom-Left placer + First-Fit selection"), ('DJD', "DJD Heuristic", "Nest using No-Fit Polygon placer + DJD Heuristic selection"), ], default='NFP', update=notify_tree_changed ) allow_rotations: bool = NfBoolProperty( name="Allow Rotations", default=True, update=notify_tree_changed ) def init(self, context): self.add_input("SLICESOCK_Mesh", "Slicing Group") self.add_input("NodeSocketFloat", "Bed X").value = 250.0 self.add_input("NodeSocketFloat", "Bed Y").value = 250.0 self.add_input("NodeSocketFloat", "Spacing").value = 2.0 self.add_output("SLICESOCK_Mesh", "Slicing Group") def draw_buttons(self, context, layout): assert layout layout.prop(self, "method", text="") layout.prop(self, "allow_rotations") def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup | None: if socket_name != "Slicing Group": return None mesh_data: SlicingCollection | SlicingGroup | None = self.resolve("Slicing Group") if not mesh_data: return None bed_x: float = self.resolve("Bed X") or 250.0 bed_y: float = self.resolve("Bed Y") or 250.0 spacing: float = self.resolve("Spacing") or 2.0 # Convert input to SlicingGroup if it's a SlicingCollection or list is_list = is_list_data(mesh_data) if isinstance(mesh_data, SlicingGroup): slicing_group = mesh_data elif is_list: slicing_group = SlicingGroup([]) for idx, item in enumerate(mesh_data): if isinstance(item, SlicingGroup): for k, sc in item.collections.items(): slicing_group.collections[f"{k}_{idx}"] = sc elif isinstance(item, SlicingCollection): col_name = item.name or f"mesh_{idx}" slicing_group.collections[col_name] = item elif isinstance(item, SlicingObject): sc = SlicingCollection([item], item.name or f"mesh_{idx}") slicing_group.collections[sc.name] = sc else: slicing_group = SlicingGroup([]) slicing_group.collections = {mesh_data.name: mesh_data} # Create a deep copy of the group to avoid mutating cached inputs copied_group = _copy_slicing_group(slicing_group) # Prepare the list of items to pack import pynest2d items_list = [] collection_map = {} for name, sc in copied_group.collections.items(): if sc.min_x is None or sc.max_x is None or sc.min_y is None or sc.max_y is None: continue w_mm = sc.max_x - sc.min_x h_mm = sc.max_y - sc.min_y if w_mm <= 0 or h_mm <= 0: continue w = pynest2d.mm(w_mm) h = pynest2d.mm(h_mm) item = pynest2d.Rectangle(w, h) items_list.append(item) collection_map[id(item)] = (name, sc) if not items_list: return copied_group # Define the bin bin_w = pynest2d.mm(bed_x) bin_h = pynest2d.mm(bed_y) bin_center = pynest2d.Point(pynest2d.mm(bed_x / 2.0), pynest2d.mm(bed_y / 2.0)) box = pynest2d.Box(bin_w, bin_h, bin_center) # Spacing in internal units spacing_units = pynest2d.mm(spacing) # Configure and run the selected placer placer = getattr(self, "method", "NFP") allow_rot = getattr(self, "allow_rotations", True) if placer == 'BL': config = pynest2d.BLConfig() config.allow_rotations = allow_rot pynest2d.nest_blp(items_list, box, spacing_units, config) elif placer == 'DJD': placer_config = pynest2d.NfpConfig() if not allow_rot: placer_config.rotations = [0.0] pynest2d.nest_djd(items_list, box, spacing_units, placer_config) else: # 'NFP' config = pynest2d.NfpConfig() if not allow_rot: config.rotations = [0.0] pynest2d.nest(items_list, box, spacing_units, config) # Apply the resulting translations and rotations to the copied collections for item in items_list: bin_id = item.binId() if bin_id == 0: name, sc = collection_map[id(item)] trans = item.translation() rot = item.rotation() tx = trans.x() / pynest2d.MM_IN_COORDS ty = trans.y() / pynest2d.MM_IN_COORDS min_x = sc.min_x min_y = sc.min_y cos_val = np.cos(rot) sin_val = np.sin(rot) for so in sc.objects: if so.mesh is None or so.mesh.size == 0: continue vx = so.mesh[:, :3, 0] vy = so.mesh[:, :3, 1] vx_new = (vx - min_x) * cos_val - (vy - min_y) * sin_val + tx vy_new = (vx - min_x) * sin_val + (vy - min_y) * cos_val + ty so.mesh[:, :3, 0] = vx_new so.mesh[:, :3, 1] = vy_new nx = so.mesh[:, 3, 0] ny = so.mesh[:, 3, 1] nx_new = nx * cos_val - ny * sin_val ny_new = nx * sin_val + ny * cos_val so.mesh[:, 3, 0] = nx_new so.mesh[:, 3, 1] = ny_new else: name, sc = collection_map[id(item)] if is_list: return list(copied_group.collections.values()) return copied_group def _copy_slicing_object(so: SlicingObject) -> SlicingObject: new_so = SlicingObject() new_so.name = so.name new_so.parent = so.parent new_so.mesh = so.mesh.copy() if so.mesh is not None else np.empty((0, 4, 3)) new_so.attributes = {} for k, v in so.attributes.items(): if isinstance(v, dict): new_so.attributes[k] = { "domain": v.get("domain", "face"), "type": v.get("type", "string"), "values": v["values"].copy() if isinstance(v.get("values"), np.ndarray) else v.get("values") } elif isinstance(v, np.ndarray): new_so.attributes[k] = v.copy() elif isinstance(v, list): new_so.attributes[k] = list(v) else: new_so.attributes[k] = copy.deepcopy(v) new_so.transforms = [dict(t) for t in getattr(so, "transforms", [])] new_so.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties() new_so.pdf_object_properties = list(getattr(so, "pdf_object_properties", [])) return new_so def _copy_slicing_collection(sc: SlicingCollection) -> SlicingCollection: new_sc = SlicingCollection([], sc.name) new_sc.objects = [_copy_slicing_object(so) for so in sc.objects] new_sc.transforms = [dict(t) for t in getattr(sc, "transforms", [])] return new_sc def _copy_slicing_group(sg: SlicingGroup) -> SlicingGroup: new_sg = SlicingGroup([]) new_sg.collections = {k: _copy_slicing_collection(sc) for k, sc in sg.collections.items()} new_sg.transforms = [dict(t) for t in getattr(sg, "transforms", [])] return new_sg def _transform_geometry_item(geom: Any, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray) -> Any: if geom is None: return None elif isinstance(geom, SlicePaths): copied_data = transform_slice_paths(geom, t_arr, r_arr, s_arr) copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr) return copied_data elif isinstance(geom, (list, tuple)): return [_transform_geometry_item(item, t_arr, r_arr, s_arr) for item in geom] elif isinstance(geom, SlicingGroup): copied_data = _copy_slicing_group(geom) for col in copied_data.collections.values(): if any(s != 1.0 for s in s_arr): col.scale(s_arr) if any(r != 0.0 for r in r_arr): col.rotate(r_arr) if any(t != 0.0 for t in t_arr): col.offset(t_arr) col.transforms = update_transform(getattr(col, "transforms", []), t_arr, r_arr, s_arr) for so in col.objects: so.transforms = update_transform(getattr(so, "transforms", []), t_arr, r_arr, s_arr) copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr) return copied_data elif isinstance(geom, SlicingCollection): copied_data = _copy_slicing_collection(geom) if any(s != 1.0 for s in s_arr): copied_data.scale(s_arr) if any(r != 0.0 for r in r_arr): copied_data.rotate(r_arr) if any(t != 0.0 for t in t_arr): copied_data.offset(t_arr) for so in copied_data.objects: so.transforms = update_transform(getattr(so, "transforms", []), t_arr, r_arr, s_arr) copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr) return copied_data elif isinstance(geom, SlicingObject): copied_data = _copy_slicing_object(geom) if any(s != 1.0 for s in s_arr): copied_data.scale(s_arr) if any(r != 0.0 for r in r_arr): copied_data.rotate(r_arr) if any(t != 0.0 for t in t_arr): copied_data.offset(t_arr) copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr) return copied_data elif hasattr(geom, "location") and hasattr(geom, "rotation_euler") and hasattr(geom, "scale") and not hasattr(geom, "mesh"): from ....infra.blender.blender_slice_nodes_generic import object_to_mesh mesh_geom = object_to_mesh(geom, include_children=True) return _transform_geometry_item(mesh_geom, t_arr, r_arr, s_arr) return geom @register_class class SLICENODE_TransformGeometry(SliceNode): bl_label = "Transform Geometry" list_socket_mapping = [("Geometry", "Geometry")] def init(self, context): self.add_input("SLICESOCK_Geometry", "Geometry") self.add_input("NodeSocketVector", "Translation") self.add_input("NodeSocketRotation", "Rotation") self.add_input("NodeSocketVector", "Scale").value = (1.0, 1.0, 1.0) self.add_output("SLICESOCK_Geometry", "Geometry") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: def _transform_one(g: Any, t: Any, r: Any, s: Any) -> Any: 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 self.eval_multivalue( _transform_one, main_socket="Geometry", secondary_sockets=["Translation", "Rotation", "Scale"], is_vector=[True, True, True], socket_name=socket_name, ) @register_class class SLICENODE_GetTransforms(SliceNode): bl_label = "Get Transforms" list_socket_mapping = [("Geometry", ["Translation", "Rotation", "Scale"])] def init(self, context): self.add_input("SLICESOCK_Geometry", "Geometry") self.add_output("NodeSocketVector", "Translation") self.add_output("NodeSocketRotation", "Rotation") self.add_output("NodeSocketVector", "Scale") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: from ....core.slice import extract_geometry_transforms from ....infra.blender.list_utils import is_input_a_list sock_lower = socket_name.lower() default = (1.0, 1.0, 1.0) if sock_lower == "scale" else (0.0, 0.0, 0.0) idx = 0 if sock_lower == "translation" else (1 if sock_lower == "rotation" else 2) geom = self.resolve("Geometry") if geom is None: return default is_list = is_input_a_list(geom, is_vector=False) if not is_list and isinstance(geom, (list, tuple)): if type(geom).__name__ != "SlicePaths" and not getattr(geom, "is_slice_paths", False): is_list = True elif not is_list and hasattr(geom, "__iter__") and not hasattr(geom, "shape") and not isinstance(geom, (str, bytes, dict, set)) and type(geom).__name__ != "SlicePaths" and not getattr(geom, "is_slice_paths", False) and not hasattr(geom, "mesh") and not hasattr(geom, "objects") and not hasattr(geom, "collections") and not hasattr(geom, "location"): is_list = True if is_list: items = list(geom) if len(items) == 0: return [] results = [] for item in items: trans = extract_geometry_transforms(item) if isinstance(trans, tuple) and len(trans) > idx: results.append(trans[idx]) else: results.append(default) return results else: trans = extract_geometry_transforms(geom) if isinstance(trans, tuple) and len(trans) > idx: return trans[idx] return default @register_class class SLICENODE_DropGeometry(SliceNode): bl_label = "Drop Geometry" list_socket_mapping = [("Mesh", ["Mesh", "Translation"])] def init(self, context): self.add_input("SLICESOCK_Mesh", "Mesh") self.add_input("NodeSocketFloat", "Z") self.add_output("SLICESOCK_Mesh", "Mesh") self.add_output("NodeSocketVector", "Translation") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: 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(z_float - min_z) if isinstance(item, SlicingGroup): copied_data = _copy_slicing_group(item) else: copied_data = _copy_slicing_collection(item) drop_vec = np.array([0.0, 0.0, offset_z], dtype=np.float64) copied_data.offset(drop_vec) copied_data.transforms = update_transform(getattr(copied_data, "transforms", []), t_arr=drop_vec) if isinstance(copied_data, SlicingGroup): for col in copied_data.collections.values(): col.transforms = update_transform(getattr(col, "transforms", []), t_arr=drop_vec) for so in col.objects: so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec) elif isinstance(copied_data, SlicingCollection): for so in copied_data.objects: so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec) return copied_data, (0.0, 0.0, offset_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 ) def format_name_template(template: str, so: Any, default_old_name: str = "") -> str: if not template or "{" not in template or "}" not in template: return template import re from ....core.pdf_exporter.metadata import get_first_attribute_value def replace_match(match: re.Match[str]) -> str: attr_name = match.group(1).strip() val = get_first_attribute_value(so, attr_name) if val is None and attr_name in ("Name", "name"): val = getattr(so, "name", "") or default_old_name if val is None: return "" if isinstance(val, float): return f"{val:g}" return str(val) return re.sub(r"\{([^}]+)\}", replace_match, template) def _rename_slicing_item(geom: Any, new_name: str) -> Any: if geom is None or not new_name: return geom elif isinstance(geom, SlicePaths): copied = geom.copy_with(paths=list(geom)) target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", "")) copied.name = target_name return copied elif isinstance(geom, (list, tuple)): return [_rename_slicing_item(item, f"{new_name}_{i}" if (len(geom) > 1 and "{" not in new_name) else new_name) for i, item in enumerate(geom)] elif isinstance(geom, SlicingGroup): copied = _copy_slicing_group(geom) old_colls = dict(copied.collections) new_colls = {} for i, (old_key, coll) in enumerate(old_colls.items()): coll_old_name = getattr(coll, "name", old_key) target_name = format_name_template(new_name, coll, default_old_name=coll_old_name) coll_new_name = f"{target_name}_{i}" if (len(old_colls) > 1 and "{" not in new_name and target_name == new_name) else target_name new_colls[coll_new_name] = _rename_slicing_item(coll, coll_new_name) copied.collections = new_colls return copied elif isinstance(geom, SlicingCollection): copied = _copy_slicing_collection(geom) target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", "")) copied.name = target_name for i, so in enumerate(copied.objects): so.parent = target_name so_old_name = getattr(so, "name", "") so_target = format_name_template(new_name, so, default_old_name=so_old_name) so.name = f"{so_target}_{i}" if (len(copied.objects) > 1 and "{" not in new_name and so_target == new_name) else so_target return copied elif isinstance(geom, SlicingObject): copied = _copy_slicing_object(geom) target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", "")) copied.name = target_name return copied return geom @register_class class SLICENODE_RenameMesh(SliceNode): bl_label = "Rename Mesh" list_socket_mapping = [("Geometry", "Geometry")] def init(self, context): self.add_input("SLICESOCK_Mesh", "Geometry") self.add_input("NodeSocketString", "Name") self.add_output("SLICESOCK_Mesh", "Geometry") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: 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 ) def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tuple[float, float, float] | None, show_name: bool) -> Any: if geom is None: return None elif isinstance(geom, SlicePaths): copied = geom.copy_with(paths=list(geom)) copied.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name) return copied elif isinstance(geom, (list, tuple)): return [_assign_pdf_properties_to_item(item, line_width, line_color, show_name) for item in geom] elif isinstance(geom, SlicingGroup): copied = _copy_slicing_group(geom) new_colls = {} for k, coll in copied.collections.items(): new_colls[k] = _assign_pdf_properties_to_item(coll, line_width, line_color, show_name) copied.collections = new_colls return copied elif isinstance(geom, SlicingCollection): copied = _copy_slicing_collection(geom) copied.objects = [_assign_pdf_properties_to_item(so, line_width, line_color, show_name) for so in copied.objects] return copied elif isinstance(geom, SlicingObject): copied = _copy_slicing_object(geom) copied.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name) return copied return geom @register_class class SLICENODE_AssignPdfProperties(SliceNode): bl_label = "Assign PDF Properties" list_socket_mapping = [("Geometry", "Geometry")] line_width: float = NfFloatProperty(name="Line Width (mm)", default=0.0, min=0.0, max=50.0) line_color: tuple[float, float, float] = NfFloatVectorProperty( name="Line Color", subtype='COLOR', default=(0.0, 0.0, 0.0), min=0.0, max=1.0, size=3 ) show_name: bool = NfBoolProperty(name="Show Name", default=False) def init(self, context): self.add_input("SLICESOCK_Mesh", "Geometry") self.add_input("NodeSocketFloat", "Line Width") self.add_output("SLICESOCK_Mesh", "Geometry") def draw_buttons(self, context, layout): assert layout layout.prop(self, "line_color") layout.prop(self, "show_name") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: lc = (float(self.line_color[0]), float(self.line_color[1]), float(self.line_color[2])) sn = bool(self.show_name) def _assign_one(g: Any, line_w: Any) -> Any: 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 self.eval_multivalue(_assign_one, "Geometry", ["Line Width"], socket_name=socket_name) def _parse_settings_props(raw_settings: Any) -> list[tuple[str, str]]: props: list[tuple[str, str]] = [] if raw_settings: if isinstance(raw_settings, dict): for k, v in raw_settings.items(): props.append((str(k), str(v))) elif isinstance(raw_settings, (list, tuple)): for item in raw_settings: if isinstance(item, tuple) and len(item) == 2: props.append((str(item[0]), str(item[1]))) elif isinstance(item, dict): for k, v in item.items(): props.append((str(k), str(v))) else: props.append(("", str(item))) return props def _is_single_settings_spec(val: Any) -> bool: if isinstance(val, dict): return True if isinstance(val, (list, tuple)): if len(val) > 0 and all(isinstance(x, tuple) and len(x) == 2 and not isinstance(x[0], (list, dict, tuple)) for x in val): return True if len(val) == 0: return True return False return True def _assign_properties_to_item(geom: Any, props: list[tuple[str, str]]) -> Any: from ....core.pdf_exporter.metadata import resolve_object_properties if geom is None: return None elif isinstance(geom, SlicePaths): copied = geom.copy_with(paths=list(geom)) copied.pdf_object_properties = resolve_object_properties(copied, props) return copied elif isinstance(geom, (list, tuple)): return [_assign_properties_to_item(item, props) for item in geom] elif isinstance(geom, SlicingGroup): copied = _copy_slicing_group(geom) for coll in copied.collections.values(): for obj in coll.objects: obj.pdf_object_properties = resolve_object_properties(obj, props) return copied elif isinstance(geom, SlicingCollection): copied = _copy_slicing_collection(geom) for obj in copied.objects: obj.pdf_object_properties = resolve_object_properties(obj, props) return copied elif isinstance(geom, SlicingObject): copied = _copy_slicing_object(geom) copied.pdf_object_properties = resolve_object_properties(geom, props) return copied return geom @register_class class SLICENODE_AssignPropertyToObject(SliceNode): bl_label = "Assign PDF Object Properties" list_socket_mapping = [("Geometry", "Geometry")] def init(self, context): self.add_input("SLICESOCK_Mesh", "Geometry") self.add_input("SLICESOCK_Settings", "Settings") self.add_output("SLICESOCK_Mesh", "Geometry") def evaluate_output(self, socket_name: str, **_kwargs) -> Any: def _assign_one(g: Any, s: Any) -> Any: props = _parse_settings_props(s) return _assign_properties_to_item(g, props) return self.eval_multivalue(_assign_one, "Geometry", ["Settings"], socket_name=socket_name)