diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index 7d4ac3c..d85a5f4 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -232,7 +232,7 @@ class SLICENODE_SeparateMeshByRange(SliceNode): # Attribute values might be floats or ints, we check for equality mask = vals == target_val - num_digits = max(2, self.end_val) + num_digits = max(2, len(str(self.end_val))) if np.all(mask): res.objects.append(so) diff --git a/core/nodeslicer/nodes/path.py b/core/nodeslicer/nodes/path.py index 93a1bc6..0611ebb 100644 --- a/core/nodeslicer/nodes/path.py +++ b/core/nodeslicer/nodes/path.py @@ -32,6 +32,10 @@ class SLICENODE_GetPaths(SliceNode): sp = SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data) if hasattr(obj, "transforms"): sp.transforms = list(getattr(obj, "transforms", [])) + if hasattr(obj, "name") and getattr(obj, "name"): + name_val = str(getattr(obj, "name")) + sp.name = name_val + sp.objects_info = {"objects": [{"name": name_val, "polygon": []}]} return sp @register_class @@ -109,6 +113,22 @@ class SLICENODE_SliceShells(SliceNode): slices.first_layer_height = fl_height slices.transforms = list(getattr(coll, "transforms", [])) or list(getattr(merged_object, "transforms", [])) + names = [] + if objects: + for o in objects: + n = getattr(o, "name", "") + if n: + if n.endswith("_merged"): + n = n[:-7] + if n and n not in names: + names.append(n) + if not names and hasattr(coll, "name") and getattr(coll, "name"): + names.append(str(getattr(coll, "name"))) + + if names: + slices.name = "-".join(names) + slices.objects_info = {"objects": [{"name": n, "polygon": []} for n in names]} + if len(z_values) > 0 and fl_height is not None and layer_height > 0: mult = fl_height / layer_height z_first = z_values[0] diff --git a/core/slice.py b/core/slice.py index 09a3680..26d9f3f 100644 --- a/core/slice.py +++ b/core/slice.py @@ -3,6 +3,7 @@ from numpy import float64 import numpy as np import pyclipper from typing import Any, Literal +import copy from shapely.geometry.base import BaseGeometry from ..infra.blender.blender_mesh_capture import PdfProperties @@ -164,8 +165,10 @@ class SlicePaths(list[np.ndarray]): transforms: list[dict] object_properties: list[tuple[str, str]] pdf_properties: PdfProperties + name: str + objects_info: dict | None - def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = [], extrusion_rate: list[np.ndarray] | None = None, feedrate: list[np.ndarray] | None = None, extra_attributes: dict[str, list[np.ndarray]] | None = None, layer_height: float = 0.2, first_layer_height: float | None = None, transforms: list[dict] | None = None): + def __init__(self, paths: list[np.ndarray] = [], groups: list[int] = [], extrusion_rate: list[np.ndarray] | None = None, feedrate: list[np.ndarray] | None = None, extra_attributes: dict[str, list[np.ndarray]] | None = None, layer_height: float = 0.2, first_layer_height: float | None = None, transforms: list[dict] | None = None, name: str = "", objects_info: dict | None = None): super().__init__(paths) self.groups = [] @@ -188,6 +191,8 @@ class SlicePaths(list[np.ndarray]): self.layer_height = layer_height self.first_layer_height = first_layer_height self.transforms = list(transforms) if transforms is not None else [] + self.name = name + self.objects_info = copy.deepcopy(objects_info) if objects_info is not None else None def copy_with(self, paths: list[np.ndarray], groups: list[int] | None = None, extrusion_rate: list[np.ndarray] | None = None, feedrate: list[np.ndarray] | None = None, extra_attributes: dict[str, list[np.ndarray]] | None = None) -> SlicePaths: """Returns a new SlicePaths object with new paths/groups, preserving all metadata and instance attributes.""" @@ -349,11 +354,30 @@ class SlicePaths(list[np.ndarray]): merged.append_with_group(path, group=g, extrusion_rate=ext, feedrate=feed, extra_attributes=extra) merged_transforms = [] + merged_names = [] + merged_objs = [] for sp in paths_list: + if hasattr(sp, "objects_info") and getattr(sp, "objects_info"): + for obj_entry in sp.objects_info.get("objects", []): + if obj_entry not in merged_objs: + merged_objs.append(dict(obj_entry)) + n_str = obj_entry.get("name", "") + if n_str and n_str not in merged_names: + merged_names.append(n_str) + elif hasattr(sp, "name") and getattr(sp, "name"): + n_str = str(getattr(sp, "name")) + if n_str and n_str not in merged_names: + merged_names.append(n_str) + merged_objs.append({"name": n_str, "polygon": []}) + for t in getattr(sp, "transforms", []): if t not in merged_transforms: merged_transforms.append(t) + merged.transforms = merged_transforms + if merged_objs: + merged.objects_info = {"objects": merged_objs} + merged.name = "-".join(merged_names) return merged @@ -1583,6 +1607,8 @@ class Pather(object): self.retraction_feedrate: float = 0.0 self.lift: float = 0.0 self.transforms: list[dict] = list(getattr(paths, "transforms", [])) + self.objects_info: dict | None = getattr(paths, "objects_info", None) + self.name: str = str(getattr(paths, "name", "")) @property def layer_width(self) -> float: @@ -1653,6 +1679,13 @@ class Pather(object): f";OFFSET_Y={offset_y:.6f}", ]) + if self.objects_info: + import json + header.append(f"; objects_info = {json.dumps(self.objects_info)}") + elif self.name: + import json + header.append(f'; objects_info = {json.dumps({"objects": [{"name": self.name, "polygon": []}]})}') + if hasattr(self, "transforms") and self.transforms: import json header.append(f";PREPARATION_TRANSFORMS={json.dumps(self.transforms)}") diff --git a/infra/networking/gcode_utils.py b/infra/networking/gcode_utils.py index e47bd97..43fe1d2 100644 --- a/infra/networking/gcode_utils.py +++ b/infra/networking/gcode_utils.py @@ -37,7 +37,7 @@ def parse_gcode_metadata(gcode: str) -> dict[str, Any]: return fields def get_gcode_name_from_metadata(gcode: str, default: str = "output") -> str: - """Generates a descriptive name for the G-code based on its objects_info metadata.""" + """Generates a descriptive name for the G-code based on its objects_info metadata or object comments.""" parts_match = re.search(r"; objects_info = (.*)", gcode) filament_match = re.search(r"; filament_type = (.*)", gcode) @@ -54,8 +54,19 @@ def get_gcode_name_from_metadata(gcode: str, default: str = "output") -> str: object_names = "-".join(f"{c}x{n}" if c > 1 else n for n, c in counts.items()) parts = [object_names, filament, printer] return "-".join([str(p) for p in parts if p]) - except: + except Exception: pass + + # Fallback search for object name comments (e.g. ; OBJECT_NAME = ..., ; object_name = ..., ; OBJECT: ...) + obj_matches = re.findall(r";\s*(?:OBJECT_NAME|object_name|OBJECT|object)\s*[:=]\s*(.+)", gcode) + if obj_matches: + names = [m.strip().split(" id:")[0] for m in obj_matches if m.strip()] + if names: + counts = Counter(names) + object_names = "-".join(f"{c}x{n}" if c > 1 else n for n, c in counts.items()) + parts = [object_names, filament, printer] + return "-".join([str(p) for p in parts if p]) + return default diff --git a/tests/test_slice.py b/tests/test_slice.py index 51c403a..b207b53 100644 --- a/tests/test_slice.py +++ b/tests/test_slice.py @@ -706,5 +706,61 @@ class TestSlice(unittest.TestCase): res_intr = node.evaluate_output("SLICESOCK_Intrinsics") self.assertEqual(res_intr.retraction_feedrate, 1800.0) + def test_gcode_name_from_metadata_paths_to_gcode(self): + from NodeSlicer.core.slice import SlicePaths, Pather + from NodeSlicer.infra.networking.gcode_utils import get_gcode_name_from_metadata + + # 1. Test SlicePaths with explicit name + p1 = np.array([[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]]) + sp = SlicePaths([p1], name="GearPart") + pather = Pather(sp) + gcode = pather.to_gcode(include_header=True) + self.assertIn("; objects_info =", gcode) + name = get_gcode_name_from_metadata(gcode) + self.assertEqual(name, "GearPart") + + # 2. Test SlicePaths with objects_info (multiple objects) + sp_multi = SlicePaths( + [p1], + objects_info={"objects": [{"name": "PartA", "polygon": []}, {"name": "PartB", "polygon": []}]} + ) + pather_multi = Pather(sp_multi) + gcode_multi = pather_multi.to_gcode(include_header=True) + self.assertIn("; objects_info =", gcode_multi) + name_multi = get_gcode_name_from_metadata(gcode_multi) + self.assertEqual(name_multi, "PartA-PartB") + + # 3. Test get_gcode_name_from_metadata fallback object comments + gcode_fallback = "; Generated Gcode\n; OBJECT_NAME = CustomBracket\nG1 X10 Y10\n" + self.assertEqual(get_gcode_name_from_metadata(gcode_fallback), "CustomBracket") + + def test_separate_mesh_by_range_padding(self): + from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_SeparateMeshByRange + from NodeSlicer.infra.blender.blender_mesh_capture import SlicingCollection, SlicingObject + + node = SLICENODE_SeparateMeshByRange() + node.end_val = 6 + + so = SlicingObject() + so.name = "CA" + so.mesh = np.zeros((4, 4, 3)) + so.attributes = { + "part_id": { + "domain": "FACE", + "type": "INT", + "values": np.array([1, 1, 2, 2]) + } + } + + coll = SlicingCollection.__new__(SlicingCollection) + coll.objects = [so] + coll.name = "TestColl" + coll.transforms = [] + node.resolve = lambda name: coll if name == "Mesh" else ("part_id" if name == "Attribute" else None) + + res_1 = node.evaluate_output("1") + self.assertIsNotNone(res_1) + self.assertEqual(res_1.objects[0].name, "CA01") + if __name__ == '__main__': unittest.main()