From 30f9d88e4c30b9b9e95026ca8f5d654b5a8d5cd1 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Thu, 6 Aug 2026 10:50:50 +0300 Subject: [PATCH] up --- core/nodeslicer/nodes/mesh.py | 181 ++++++++++++++++++++++++------- core/nodeslicer/nodes/output.py | 16 ++- core/nodeslicer/nodes/preview.py | 13 ++- core/pdf_exporter/__init__.py | 1 + core/pdf_exporter/metadata.py | 49 +++++++++ core/pdf_exporter/writer.py | 14 ++- doc/NODES.md | 6 +- tests/test_pdf_exporter.py | 77 +++++++++++++ tests/test_transform.py | 102 +++++++++++++++++ 9 files changed, 405 insertions(+), 54 deletions(-) diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index eb18b1d..b4e53ef 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -444,23 +444,157 @@ def _copy_slicing_group(sg: SlicingGroup) -> SlicingGroup: return new_sg +def _transform_slice_paths(sp: SlicePaths, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray) -> SlicePaths: + new_paths = [] + has_scale = any(s != 1.0 for s in s_arr) + has_rot = any(r != 0.0 for r in r_arr) + has_trans = any(t != 0.0 for t in t_arr) + + if not (has_scale or has_rot or has_trans): + return sp.copy_with(paths=list(sp)) + + R = None + if has_rot: + rx, ry, rz = r_arr + cx, sx = np.cos(rx), np.sin(rx) + cy, sy = np.cos(ry), np.sin(ry) + cz, sz = np.cos(rz), np.sin(rz) + + Rx = np.array([ + [1.0, 0.0, 0.0], + [0.0, cx, -sx], + [0.0, sx, cx] + ], dtype=np.float64) + Ry = np.array([ + [cy, 0.0, sy], + [0.0, 1.0, 0.0], + [-sy, 0.0, cy] + ], dtype=np.float64) + Rz = np.array([ + [cz, -sz, 0.0], + [sz, cz, 0.0], + [0.0, 0.0, 1.0] + ], dtype=np.float64) + R = Rz @ (Ry @ Rx) + + for p in sp: + if len(p) == 0: + new_paths.append(p.copy()) + continue + + new_p = p.astype(np.float64, copy=True) + dim = new_p.shape[1] if new_p.ndim > 1 else 0 + + if has_scale and dim > 0: + if dim >= 3: + new_p[:, :3] *= s_arr + else: + new_p[:, :dim] *= s_arr[:dim] + + if has_rot and R is not None and dim > 0: + if dim >= 3: + new_p[:, :3] = new_p[:, :3] @ R.T + elif dim == 2: + rot_2d = R[:2, :2] + new_p[:, :2] = new_p[:, :2] @ rot_2d.T + + if has_trans and dim > 0: + if dim >= 3: + new_p[:, :3] += t_arr + else: + new_p[:, :dim] += t_arr[:dim] + + new_paths.append(new_p) + + return sp.copy_with(paths=new_paths) + + +def _transform_geometry_item(geom: Any, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray, t_rec: dict) -> Any: + if geom is None: + return None + elif isinstance(geom, SlicePaths): + copied_data = _transform_slice_paths(geom, t_arr, r_arr, s_arr) + if not hasattr(copied_data, "transforms") or copied_data.transforms is None: + copied_data.transforms = [] + else: + copied_data.transforms = [dict(t) for t in copied_data.transforms] + copied_data.transforms.append(t_rec) + return copied_data + elif isinstance(geom, (list, tuple)): + return [_transform_geometry_item(item, t_arr, r_arr, s_arr, t_rec) 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) + if any(t != 0.0 for t in t_arr): + copied_data.wipe_tower_xy = copied_data.wipe_tower_xy + t_arr[0:2] + if any(r != 0.0 for r in r_arr): + rz = r_arr[2] + cz, sz = np.cos(rz), np.sin(rz) + rot_mat_2d = np.array([[cz, -sz], [sz, cz]]) + copied_data.wipe_tower_xy = copied_data.wipe_tower_xy @ rot_mat_2d.T + import math + copied_data.wipe_tower_rotation_deg = round(copied_data.wipe_tower_rotation_deg + rz * 180 / math.pi, 5) + if any(s != 1.0 for s in s_arr): + copied_data.wipe_tower_xy *= s_arr[0:2] + + if not hasattr(copied_data, "transforms") or copied_data.transforms is None: + copied_data.transforms = [] + copied_data.transforms.append(t_rec) + 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) + + if not hasattr(copied_data, "transforms") or copied_data.transforms is None: + copied_data.transforms = [] + copied_data.transforms.append(t_rec) + 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) + + if not hasattr(copied_data, "transforms") or copied_data.transforms is None: + copied_data.transforms = [] + copied_data.transforms.append(t_rec) + return copied_data + return geom + + @register_class class SLICENODE_TransformGeometry(SliceNode): bl_label = "Transform Geometry" def init(self, context): - self.add_input("SLICESOCK_Mesh", "Mesh") + self.add_input("SLICESOCK_Geometry", "Geometry") self.add_input("SLICESOCK_Vector", "Translation") self.add_input("SLICESOCK_VectorRotation", "Rotation") self.add_input("SLICESOCK_Vector", "Scale").value = (1.0, 1.0, 1.0) - self.add_output("SLICESOCK_Mesh", "Mesh") + self.add_output("SLICESOCK_Geometry", "Geometry") - def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | SlicingGroup | None: - if socket_name != "Mesh": + def evaluate_output(self, socket_name: str, **_kwargs) -> Any: + if socket_name not in ("Geometry", "Mesh"): return None - mesh_data: SlicingCollection | SlicingGroup | None = self.resolve("Mesh") - if not mesh_data: + geom_data: Any = self.resolve("Geometry") + if geom_data is None: + geom_data = self.resolve("Mesh") + if geom_data is None: return None translation = self.resolve("Translation") or (0.0, 0.0, 0.0) @@ -471,47 +605,14 @@ class SLICENODE_TransformGeometry(SliceNode): r_arr = np.array(rotation, dtype=np.float64) s_arr = np.array(scale, dtype=np.float64) - if isinstance(mesh_data, SlicingGroup): - copied_data = _copy_slicing_group(mesh_data) - 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) - # Also offset wipe tower location - if any(t != 0.0 for t in t_arr): - copied_data.wipe_tower_xy = copied_data.wipe_tower_xy + t_arr[0:2] - if any(r != 0.0 for r in r_arr): - rz = r_arr[2] - cz, sz = np.cos(rz), np.sin(rz) - rot_mat_2d = np.array([[cz, -sz], [sz, cz]]) - copied_data.wipe_tower_xy = copied_data.wipe_tower_xy @ rot_mat_2d.T - import math - copied_data.wipe_tower_rotation_deg = round(copied_data.wipe_tower_rotation_deg + rz * 180 / math.pi, 5) - if any(s != 1.0 for s in s_arr): - copied_data.wipe_tower_xy *= s_arr[0:2] - else: - copied_data = _copy_slicing_collection(mesh_data) - 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) - t_rec = { "type": "transform_geometry", "translation": (float(t_arr[0]), float(t_arr[1]), float(t_arr[2])), "rotation": (float(r_arr[0]), float(r_arr[1]), float(r_arr[2])), "scale": (float(s_arr[0]), float(s_arr[1]), float(s_arr[2])), } - if not hasattr(copied_data, "transforms"): - copied_data.transforms = [] - copied_data.transforms.append(t_rec) - return copied_data + return _transform_geometry_item(geom_data, t_arr, r_arr, s_arr, t_rec) @register_class diff --git a/core/nodeslicer/nodes/output.py b/core/nodeslicer/nodes/output.py index c07840b..5161c4b 100644 --- a/core/nodeslicer/nodes/output.py +++ b/core/nodeslicer/nodes/output.py @@ -461,10 +461,10 @@ class SLICENODE_MeshToPdf(ExecSliceNode): def evaluate(self, operator=None): import datetime - from ...pdf_exporter import extract_unique_top_view_edges, export_mesh_to_pdf + from ...pdf_exporter import extract_unique_top_view_edges, export_mesh_to_pdf, get_first_object_name geom = self.resolve("Geometry") - slice_paths = self.resolve("Paths") or self.resolve("SlicePaths") + slice_paths = self.resolve("Paths") folder: str = self.resolve("Folder") or "" # Collect raw geometry items @@ -556,6 +556,16 @@ class SLICENODE_MeshToPdf(ExecSliceNode): current_raw_geom = None current_slice_paths = target_item + current_header_info = dict(header_info) + part_name_prop = (self.header_part_name or "").strip() + if not part_name_prop: + default_part_name = ( + get_first_object_name(target_item) + or get_first_object_name(current_raw_geom) + or get_first_object_name(current_slice_paths) + ) + current_header_info["part_name"] = default_part_name + edges_2d = np.empty((0, 2, 2), dtype=np.float64) if current_raw_geom is not None: raw_e = extract_unique_top_view_edges(current_raw_geom) @@ -580,7 +590,7 @@ class SLICENODE_MeshToPdf(ExecSliceNode): draw_bbox=self.draw_bbox, draw_sa2_holes=self.draw_sa2_holes, draw_header=self.draw_header, - header_info=header_info, + header_info=current_header_info, crease_angle=self.crease_angle, margin_mm=self.margin, font_size_mm=self.font_size * 2.834645669291339, diff --git a/core/nodeslicer/nodes/preview.py b/core/nodeslicer/nodes/preview.py index 93b7f17..968c0ba 100644 --- a/core/nodeslicer/nodes/preview.py +++ b/core/nodeslicer/nodes/preview.py @@ -231,16 +231,21 @@ class SLICENODE_GcodeToMesh(ExecSliceNode): mod["Socket_9"] = tuple(map(float, prep_scale)) def evaluate(self, operator=None): - gcodes = self.resolve_multi_input("Gcode") + gcodes: list[str] = [g for g in (self.resolve_multi_input("Gcode") or []) if g] base_mesh_name: str = self.resolve("Name") if not base_mesh_name: base_mesh_name = "GcodePreview" - for i, gcode in enumerate(gcodes): - if not gcode: continue + total_parts = len(gcodes) + num_digits = max(2, len(str(total_parts))) + + for i, gcode in enumerate(gcodes): + if total_parts > 1: + mesh_name = f"{base_mesh_name}{i+1:0{num_digits}d}" + else: + mesh_name = base_mesh_name - mesh_name = base_mesh_name if i == 0 else f"{base_mesh_name}_{i+1}" try: self._process_gcode(gcode, mesh_name, operator) except Exception as e: diff --git a/core/pdf_exporter/__init__.py b/core/pdf_exporter/__init__.py index a015a7e..08dd7e6 100644 --- a/core/pdf_exporter/__init__.py +++ b/core/pdf_exporter/__init__.py @@ -20,6 +20,7 @@ from .geometry import ( ) from .metadata import ( get_first_attribute_value, + get_first_object_name, extract_settings_items, extract_object_label_info, extract_object_name_positions, diff --git a/core/pdf_exporter/metadata.py b/core/pdf_exporter/metadata.py index 046f350..5f02c85 100644 --- a/core/pdf_exporter/metadata.py +++ b/core/pdf_exporter/metadata.py @@ -59,6 +59,55 @@ def get_first_attribute_value(so_item: Any, attr_name: str) -> Any: return None +def get_first_object_name(mesh_data: Any) -> str: + """ + Extracts the name of the first SlicingObject, SlicePaths, SlicingCollection, + or Blender Object in mesh_data. + """ + from ...infra.blender.blender_mesh_capture import SlicingGroup, SlicingCollection, SlicingObject + from ..slice import SlicePaths + + if mesh_data is None: + return "" + + if isinstance(mesh_data, (SlicingObject, SlicePaths)): + if getattr(mesh_data, "name", ""): + return str(mesh_data.name) + return "" + + if isinstance(mesh_data, (list, tuple)) and not isinstance(mesh_data, SlicePaths): + for sub in mesh_data: + name = get_first_object_name(sub) + if name: + return name + return "" + + if isinstance(mesh_data, SlicingGroup) and mesh_data.collections: + for key, coll in mesh_data.collections.items(): + name = get_first_object_name(coll) + if name: + return name + if key: + return str(key) + return "" + + if isinstance(mesh_data, SlicingCollection): + if mesh_data.objects: + for so in mesh_data.objects: + name = get_first_object_name(so) + if name: + return name + if getattr(mesh_data, "name", ""): + return str(mesh_data.name) + return "" + + if hasattr(mesh_data, "name") and getattr(mesh_data, "name"): + return str(getattr(mesh_data, "name")) + + return "" + + + def resolve_object_properties(so_item: Any, props: list[tuple[str, str]]) -> list[tuple[str, str]]: """ Resolves property tuples for a SlicingObject. diff --git a/core/pdf_exporter/writer.py b/core/pdf_exporter/writer.py index 4e34492..e912964 100644 --- a/core/pdf_exporter/writer.py +++ b/core/pdf_exporter/writer.py @@ -96,7 +96,8 @@ def render_technical_header_table( # Extract field values with fallbacks company_val = str(header_info.get("company", "") or "-") - part_name_val = str(header_info.get("part_name", "") or obj_name or "-") + raw_part_name = header_info.get("part_name", "") + part_name_val = str((raw_part_name or "").strip() or obj_name or "-") dwg_val = str(header_info.get("drawing_number", "") or "-") author_val = str(header_info.get("author", "") or "-") rev_val = str(header_info.get("revision", "") or "A") @@ -647,11 +648,16 @@ class PDFHeaderTableElement(PDFElement): if not exp.draw_header or not exp.header_info: return + from .metadata import get_first_object_name + primary_obj_name = "" if exp.slicing_objs: - primary_obj_name = str(getattr(exp.slicing_objs[0], "name", "")) - elif exp.raw_geometry is not None and hasattr(exp.raw_geometry, "name"): - primary_obj_name = str(getattr(exp.raw_geometry, "name", "")) + for so in exp.slicing_objs: + if hasattr(so, "name") and getattr(so, "name"): + primary_obj_name = str(getattr(so, "name")) + break + if not primary_obj_name: + primary_obj_name = get_first_object_name(exp.raw_geometry) or get_first_object_name(exp.slice_paths) render_technical_header_table( s=s, diff --git a/doc/NODES.md b/doc/NODES.md index 9325a98..49bdc46 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -70,8 +70,8 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Assign Property to Object**: Assigns custom settings/properties to geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) for rendering in PDF properties tables. - **Geometry**: Generic geometry input socket (accepts `SLICESOCK_Mesh`, `SLICESOCK_Paths`, or `SLICESOCK_Geometry`). - **Settings**: Input socket accepting configuration settings or property items. -- **Transform Geometry**: Moves, rotates, and scales a `SlicingCollection` or `SlicingGroup` relative to the origin. - - **Mesh**: Input mesh data. +- **Transform Geometry**: Moves, rotates, and scales geometry (`SlicingObject`, `SlicingCollection`, `SlicingGroup`, or `SlicePaths`) relative to the origin. + - **Geometry**: Input geometry data. - **Translation**: 3D vector specifying the translation offset. - **Rotation**: 3D vector/Euler rotation values. - **Scale**: 3D vector specifying scaling factors along the X, Y, and Z axes (defaults to `1.0, 1.0, 1.0`). @@ -128,7 +128,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Gcode to File**: Writes G-code string to a file on disk. - **Automatic Binary Conversion**: If the G-code contains the comment `; binary_gcode = 1`, the node automatically converts the output to Prusa's binary G-code format (`.bgcode`) using MeatPack encoding and Heatshrink compression. - **Mesh to PDF**: Generates a 1:1 true-scale 2D top-view (+Z orthographic) vector PDF from a `SlicingGroup` or `SlicingCollection` with a custom grid and background color. Subpanel menus are collapsed by default. - - **Technical Drawing Header**: Collapsible menu containing string inputs for technical drawing info (Company, Part Name, Drawing No., Author, Revision, Material, Tolerance). Automatically computes ISO date and scale length (`1:1`). Renders an ISO/DIN technical drawing header table on the bottom-right of the drawing. + - **Technical Drawing Header**: Collapsible menu containing string inputs for technical drawing info (Company, Part Name, Drawing No., Author, Revision, Material, Tolerance). If Part Name is left empty, it automatically defaults to the name of the first object being rendered. Automatically computes ISO date and scale length (`1:1`). Renders an ISO/DIN technical drawing header table on the bottom-right of the drawing. - **Geometry**: Input socket accepting a single geometry (`SlicingGroup`, `SlicingCollection`, or `SlicingObject`). Join multiple geometries using Join Geometry nodes. - **SlicePaths**: Input socket accepting `SlicePaths` (reserved for path rendering). - **Folder**: Target folder or PDF file path on disk. diff --git a/tests/test_pdf_exporter.py b/tests/test_pdf_exporter.py index 536d147..29a83fa 100644 --- a/tests/test_pdf_exporter.py +++ b/tests/test_pdf_exporter.py @@ -1427,6 +1427,83 @@ class TestPdfExporter(unittest.TestCase): self.assertIn(" m ", content) + def test_get_first_object_name(self): + from NodeSlicer.core.pdf_exporter import get_first_object_name + from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject, SlicingGroup, SlicingCollection + from NodeSlicer.core.slice import SlicePaths + + so = SlicingObject() + so.name = "FirstObj" + self.assertEqual(get_first_object_name(so), "FirstObj") + + sp = SlicePaths() + sp.name = "PathObj" + self.assertEqual(get_first_object_name(sp), "PathObj") + + self.assertEqual(get_first_object_name([None, so]), "FirstObj") + + coll = SlicingCollection.__new__(SlicingCollection) + coll.objects = [so] + coll.name = "CollName" + group = SlicingGroup.__new__(SlicingGroup) + group.collections = {"GroupKey": coll} + self.assertEqual(get_first_object_name(group), "FirstObj") + + def test_mesh_to_pdf_default_part_name_first_object(self): + from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_MeshToPdf + from NodeSlicer.infra.blender.blender_mesh_capture import SlicingObject + + node = SLICENODE_MeshToPdf.__new__(SLICENODE_MeshToPdf) + node.draw_wireframe = True + node.draw_boundary = False + node.draw_contour = False + node.draw_crease = False + node.crease_angle = 30.0 + node.draw_bbox = False + node.draw_sa2_holes = False + node.draw_header = True + node.header_company = "" + node.header_part_name = "" # Empty field -> should default to object name + node.header_drawing_number = "" + node.header_author = "" + node.header_revision = "A" + node.header_material = "" + node.header_tolerance = "" + node.font_size = 0.0 + node.margin = 10.0 + node.wire_color = (0.0, 0.0, 0.0) + node.bg_color = (1.0, 1.0, 1.0) + node.bbox_color = (0.0, 0.0, 1.0) + + class MockOperator: + def __init__(self): + self.reports = [] + def report(self, type_set, message): + self.reports.append((type_set, message)) + + op = MockOperator() + so = SlicingObject() + so.name = "GearWheel" + so.mesh = np.array([[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0]]], dtype=np.float64) + + with tempfile.TemporaryDirectory() as tmpdir: + pdf_file = os.path.join(tmpdir, "output.pdf") + resolved_inputs = { + "Geometry": so, + "Paths": None, + "Folder": pdf_file, + "Settings": None, + } + node.resolve = lambda fieldname: resolved_inputs.get(fieldname) + + res = node.evaluate(operator=op) + self.assertEqual(res, "ok") + self.assertTrue(os.path.exists(pdf_file)) + with open(pdf_file, "rb") as f: + content = decompress_pdf_streams(f.read()) + self.assertIn("(GearWheel) Tj", content) + + if __name__ == '__main__': unittest.main() diff --git a/tests/test_transform.py b/tests/test_transform.py index bcb1526..4cf5ab4 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -201,6 +201,66 @@ class TestTransform(unittest.TestCase): # v0 becomes [6.0, 22.0, 36.0] np.testing.assert_allclose(result_sc.objects[0].mesh[0, 0], [6.0, 22.0, 36.0], atol=1e-7) + def test_transform_node_slice_paths(self): + from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_TransformGeometry + from NodeSlicer.core.slice import SlicePaths + + path = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float64) + sp = SlicePaths([path]) + + node = SLICENODE_TransformGeometry() + node.resolve = lambda f: { + "Geometry": sp, + "Translation": (10.0, 20.0, 30.0), + "Rotation": (0.0, 0.0, np.pi / 2.0), + "Scale": (2.0, 2.0, 2.0) + }.get(f) + + result_sp = node.evaluate_output("Geometry") + self.assertIsNotNone(result_sp) + self.assertIsInstance(result_sp, SlicePaths) + self.assertIsNot(result_sp, sp) + + # Point 1: [1, 2, 3] -> scale [2, 4, 6] -> rot Z pi/2 [-4, 2, 6] -> trans [6, 22, 36] + np.testing.assert_allclose(result_sp[0][0], [6.0, 22.0, 36.0], atol=1e-7) + # Point 2: [4, 5, 6] -> scale [8, 10, 12] -> rot Z pi/2 [-10, 8, 12] -> trans [0, 28, 42] + np.testing.assert_allclose(result_sp[0][1], [0.0, 28.0, 42.0], atol=1e-7) + + # Check transforms metadata + self.assertEqual(len(result_sp.transforms), 1) + self.assertEqual(result_sp.transforms[0]["type"], "transform_geometry") + + def test_transform_node_list_input(self): + from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_TransformGeometry + from NodeSlicer.core.slice import SlicePaths + + so = SlicingObject() + so.mesh = np.array([[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0], [0.0, 0.0, 1.0]]], dtype=np.float64) + sc = SlicingCollection([], "coll") + sc.objects = [so] + + path = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float64) + sp = SlicePaths([path]) + + geom_list = [sc, sp] + + node = SLICENODE_TransformGeometry() + node.resolve = lambda f: { + "Geometry": geom_list, + "Translation": (10.0, 0.0, 0.0), + "Rotation": (0.0, 0.0, 0.0), + "Scale": (1.0, 1.0, 1.0) + }.get(f) + + result_list = node.evaluate_output("Geometry") + self.assertIsInstance(result_list, list) + self.assertEqual(len(result_list), 2) + self.assertIsInstance(result_list[0], SlicingCollection) + self.assertIsInstance(result_list[1], SlicePaths) + + np.testing.assert_allclose(result_list[0].objects[0].mesh[0, 0], [11.0, 2.0, 3.0]) + np.testing.assert_allclose(result_list[1][0][0], [11.0, 2.0, 3.0]) + def test_drop_geometry_node(self): from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_DropGeometry @@ -364,6 +424,48 @@ class TestTransform(unittest.TestCase): self.assertEqual(tuple(mock_mod_dict.get("Socket_6")), (0.15, 0.25, 0.35)) self.assertEqual(tuple(mock_mod_dict.get("Socket_9")), (1.5, 2.0, 2.5)) + def test_gcode_to_mesh_naming_convention(self): + from NodeSlicer.core.nodeslicer.nodes.preview import SLICENODE_GcodeToMesh + node = SLICENODE_GcodeToMesh() + + # 1. Single part - no number suffix + node.resolve_multi_input = MagicMock(return_value=["GCODE1"]) + node.resolve = MagicMock(return_value="Part") + node._process_gcode = MagicMock() + + node.evaluate() + node._process_gcode.assert_called_once_with("GCODE1", "Part", None) + + # 2. Three parts - padded to 2 digits (01, 02, 03) + node.resolve_multi_input = MagicMock(return_value=["GC1", "GC2", "GC3"]) + node.resolve = MagicMock(return_value="Part") + node._process_gcode = MagicMock() + + node.evaluate() + self.assertEqual(node._process_gcode.call_count, 3) + node._process_gcode.assert_any_call("GC1", "Part01", None) + node._process_gcode.assert_any_call("GC2", "Part02", None) + node._process_gcode.assert_any_call("GC3", "Part03", None) + + # 3. Fifteen parts - padded to 2 digits (01..15) + node.resolve_multi_input = MagicMock(return_value=[f"GC{i}" for i in range(1, 16)]) + node.resolve = MagicMock(return_value="Part") + node._process_gcode = MagicMock() + + node.evaluate() + self.assertEqual(node._process_gcode.call_count, 15) + node._process_gcode.assert_any_call("GC1", "Part01", None) + node._process_gcode.assert_any_call("GC15", "Part15", None) + + # 4. Default base name when empty name provided + node.resolve_multi_input = MagicMock(return_value=["GC1", "GC2"]) + node.resolve = MagicMock(return_value="") + node._process_gcode = MagicMock() + + node.evaluate() + node._process_gcode.assert_any_call("GC1", "GcodePreview01", None) + node._process_gcode.assert_any_call("GC2", "GcodePreview02", None) + if __name__ == '__main__': unittest.main()