output naming

This commit is contained in:
Nicolas Predella 2026-10-09 13:53:39 +03:00
parent 45d47161f8
commit b09a5fb636
3 changed files with 89 additions and 2 deletions

View file

@ -20,6 +20,7 @@ class SLICENODE_GcodeToFile(ExecSliceNode):
def init(self, context): def init(self, context):
self._init_exec() self._init_exec()
self.add_input("NodeSocketString", "Folder") self.add_input("NodeSocketString", "Folder")
self.add_input("NodeSocketString", "Name")
self.add_multi_socket_group("NodeSocketString", name="Gcode") self.add_multi_socket_group("NodeSocketString", name="Gcode")
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
@ -38,7 +39,17 @@ class SLICENODE_GcodeToFile(ExecSliceNode):
if operator: operator.report({'ERROR'}, "No Folder provided") if operator: operator.report({'ERROR'}, "No Folder provided")
return 'error' return 'error'
base_name: str = self.resolve("Name")
total_parts: int = len(gcodes)
num_digits: int = max(2, len(str(total_parts)))
for i, gc in enumerate(gcodes): for i, gc in enumerate(gcodes):
if base_name:
if total_parts > 1:
name = f"{base_name}{i:0{num_digits}d}"
else:
name = base_name
else:
name = get_gcode_name_from_metadata(gc, default=f'object_{i}') name = get_gcode_name_from_metadata(gc, default=f'object_{i}')
# User's requested logic: .split('.')[0] # User's requested logic: .split('.')[0]

View file

@ -171,7 +171,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **Join Execution**: Synchronizes sequential execution flows. - **Join Execution**: Synchronizes sequential execution flows.
### Action & I/O ### Action & I/O
- **Gcode to File**: Writes G-code string to a file on disk. - **Gcode to File**: Writes G-code string to a file on disk. Accepts an optional **Name** input (which applies zero-padded digit suffixes when multiple G-codes are supplied, matching the preview mesh logic). If not plugged, uses the object names extracted from G-code metadata.
- **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. - **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.
- **Gcode to Text Block**: Creates a new Blender text block containing the G-code and opens it in any active text editors. - **Gcode to Text Block**: Creates a new Blender text block containing the G-code and opens it in any active text editors.
- **Start Print**: Sends G-code directly to a network printer. - **Start Print**: Sends G-code directly to a network printer.

View file

@ -466,6 +466,82 @@ class TestTransform(unittest.TestCase):
node._process_gcode.assert_any_call("GC1", "GcodePreview00", None) node._process_gcode.assert_any_call("GC1", "GcodePreview00", None)
node._process_gcode.assert_any_call("GC2", "GcodePreview01", None) node._process_gcode.assert_any_call("GC2", "GcodePreview01", None)
def test_gcode_to_file_naming_convention(self):
from unittest.mock import patch, mock_open
from NodeSlicer.core.nodeslicer.nodes.output import SLICENODE_GcodeToFile
node = SLICENODE_GcodeToFile()
mock_bpy.path.abspath = lambda p: p.replace("//", "/workspace/")
# 1. Single part with Name plugged - no number suffix
with patch("NodeSlicer.infra.networking.gcode_utils.prepare_gcode_file", return_value=(b"GC", "Part.gcode")) as mock_prep, \
patch("builtins.open", mock_open()), \
patch("os.makedirs"):
def resolve_side_effect(name):
if name == "Folder": return "output"
if name == "Name": return "Part"
return ""
node.resolve = MagicMock(side_effect=resolve_side_effect)
node.resolve_multi_input = MagicMock(return_value=["GCODE1"])
res = node.evaluate()
self.assertEqual(res, 'ok')
mock_prep.assert_called_once_with("GCODE1", "Part")
# 2. Three parts with Name plugged - padded to 2 digits (00, 01, 02)
with patch("NodeSlicer.infra.networking.gcode_utils.prepare_gcode_file", return_value=(b"GC", "test.gcode")) as mock_prep, \
patch("builtins.open", mock_open()), \
patch("os.makedirs"):
def resolve_side_effect(name):
if name == "Folder": return "output"
if name == "Name": return "Part"
return ""
node.resolve = MagicMock(side_effect=resolve_side_effect)
node.resolve_multi_input = MagicMock(return_value=["GC1", "GC2", "GC3"])
res = node.evaluate()
self.assertEqual(res, 'ok')
self.assertEqual(mock_prep.call_count, 3)
mock_prep.assert_any_call("GC1", "Part00")
mock_prep.assert_any_call("GC2", "Part01")
mock_prep.assert_any_call("GC3", "Part02")
# 3. Fifteen parts with Name plugged - padded to 2 digits (00..14)
with patch("NodeSlicer.infra.networking.gcode_utils.prepare_gcode_file", return_value=(b"GC", "test.gcode")) as mock_prep, \
patch("builtins.open", mock_open()), \
patch("os.makedirs"):
def resolve_side_effect(name):
if name == "Folder": return "output"
if name == "Name": return "Bracket"
return ""
node.resolve = MagicMock(side_effect=resolve_side_effect)
node.resolve_multi_input = MagicMock(return_value=[f"GC{i}" for i in range(1, 16)])
res = node.evaluate()
self.assertEqual(res, 'ok')
self.assertEqual(mock_prep.call_count, 15)
mock_prep.assert_any_call("GC1", "Bracket00")
mock_prep.assert_any_call("GC15", "Bracket14")
# 4. Not plugged / empty name - uses object names from metadata
gc_meta1 = '; objects_info = {"objects": [{"name": "CustomPart"}]}\nG1 X10'
gc_meta2 = 'G1 X20'
with patch("NodeSlicer.infra.networking.gcode_utils.prepare_gcode_file", return_value=(b"GC", "test.gcode")) as mock_prep, \
patch("builtins.open", mock_open()), \
patch("os.makedirs"):
def resolve_side_effect(name):
if name == "Folder": return "output"
if name == "Name": return ""
return ""
node.resolve = MagicMock(side_effect=resolve_side_effect)
node.resolve_multi_input = MagicMock(return_value=[gc_meta1, gc_meta2])
res = node.evaluate()
self.assertEqual(res, 'ok')
self.assertEqual(mock_prep.call_count, 2)
mock_prep.assert_any_call(gc_meta1, "CustomPart")
mock_prep.assert_any_call(gc_meta2, "object_1")
def test_drop_geometry_applied_to_main_transform_no_double_accumulation(self): def test_drop_geometry_applied_to_main_transform_no_double_accumulation(self):
"""Verifies that dropping geometry updates the single main transform without accumulating duplicate records.""" """Verifies that dropping geometry updates the single main transform without accumulating duplicate records."""
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_TransformGeometry, SLICENODE_DropGeometry from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_TransformGeometry, SLICENODE_DropGeometry