From fdf49ef611c41ac6cce39368717f2d2d00c864e1 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Wed, 20 May 2026 21:37:49 +0300 Subject: [PATCH] multi gcode for file save --- core/nodeslicer/nodes.py | 104 +++++++++---------- core/nodeslicer/sockets.py | 7 ++ infra/blender/blender_slice_nodes_generic.py | 58 +++++++++-- registry.py | 4 +- 4 files changed, 107 insertions(+), 66 deletions(-) diff --git a/core/nodeslicer/nodes.py b/core/nodeslicer/nodes.py index df3706b..a3e885f 100644 --- a/core/nodeslicer/nodes.py +++ b/core/nodeslicer/nodes.py @@ -524,7 +524,6 @@ class SLICENODE_Infill(SliceNode): update=notify_tree_changed ) # pyright: ignore[reportInvalidTypeForm] - def init(self, context): self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Float", "Density").value = 2.0 @@ -680,26 +679,7 @@ class SliceNodeJoiner(SliceNode): def init(self, context): self.add_output(self.output_socket_type or self.socket_type) - - def update(self) -> None: - def is_active(input) -> bool: - a: bool = input.is_linked or getattr(input, 'value', False) - b: bool = getattr(self.inputs[-1], 'value', False) - return a or b - - if len(self.inputs) == 0: - self.add_input(self.socket_type) - return - - if is_active(self.inputs[-1]): - self.add_input(self.socket_type) - - while True: - if len(self.inputs) < 2: break - if not is_active(self.inputs[-1]) and not is_active(self.inputs[-2]): - self.inputs.remove(self.inputs[-2]) - continue - break + self.add_multi_input(self.socket_type) @register_class class SLICENODE_JoinGcode(SliceNodeJoiner): @@ -975,8 +955,8 @@ class SLICENODE_GcodeToFile(ExecSliceNode): def init(self, context): self._init_exec() - self.add_input("SLICESOCK_Gcode") - self.add_input("SLICESOCK_FilePath", "Filename") + self.add_input("SLICESOCK_DirPath", "Folder") + self.add_multi_input("SLICESOCK_Gcode", "Gcode") def draw_buttons(self, context, layout): super().draw_buttons(context, layout) @@ -985,42 +965,57 @@ class SLICENODE_GcodeToFile(ExecSliceNode): from ...infra.networking.bgcode import convert_to_bgcode from pathlib import Path - gcode: str = resolve_input(self.inputs.get("Gcode")) - filename: str = resolve_input(self.inputs.get("Filename")) - if not gcode: + gcodes: str = self.resolve_multi_input('Gcode') + + folder: str = resolve_input(self.inputs.get("Folder")) + if not gcodes: if operator: operator.report({'ERROR'}, "No Gcode to save") return 'error' - if not filename: - if operator: operator.report({'ERROR'}, "No Filename provided") + if not folder: + if operator: operator.report({'ERROR'}, "No Folder provided") return 'error' - # User's requested logic: .split('.')[0] - filepath_abs = bpy.path.abspath(filename) - dirname = os.path.dirname(filepath_abs) - basename = os.path.basename(filepath_abs) - name_part = basename.split('.')[0] - os.makedirs(dirname, exist_ok=True) - - # Detect binary trigger in gcode comments - # Looking for "; binary_gcode = 1" - is_binary = "; binary_gcode = 1" in gcode - - try: - if is_binary: - filepath = os.path.join(dirname, name_part + ".bgcode") - content = convert_to_bgcode(gcode, name_part, True) - with open(filepath, "wb") as f: - f.write(content) - if operator: operator.report({'INFO'}, f"Binary G-code generated at {filepath}") + for i, gc in enumerate(gcodes): + + import re, json + parts_match = re.search(r"; objects_info = (.*)", gc) + name: str = "" + if parts_match: + gcode_info = json.loads(parts_match.group(1)) + names: list[str] = [str(v['name']) for v in gcode_info['objects']] + from collections import Counter + counts = Counter(names) + name = "-".join(f"{c}x{n}" if c > 1 else n for n, c in counts.items()) else: - filepath = os.path.join(dirname, name_part + ".gcode") - with open(filepath, "w", encoding="utf-8", newline="\n") as f: - f.write(gcode) - if operator: operator.report({'INFO'}, f"G-code saved to {filepath}") - return 'ok' - except Exception as e: - if operator: operator.report({'ERROR'}, str(e)) - return 'error' + name = 'object_'+str(i) + + # User's requested logic: .split('.')[0] + folder_path = Path(bpy.path.abspath(f"//{folder}")) + filepath_abs = folder_path / name + dirname = os.path.dirname(filepath_abs) + basename = os.path.basename(filepath_abs) + name_part = basename.split('.')[0] + os.makedirs(dirname, exist_ok=True) + + # Detect binary trigger in gcode comments + is_binary = "; binary_gcode = 1" in gc + + try: + if is_binary: + filepath = os.path.join(dirname, name_part + ".bgcode") + content = convert_to_bgcode(gc, name_part, True) + with open(filepath, "wb") as f: + f.write(content) + if operator: operator.report({'INFO'}, f"Binary G-code generated at {filepath}") + else: + filepath = os.path.join(dirname, name_part + ".gcode") + with open(filepath, "w", encoding="utf-8", newline="\n") as f: + f.write(gc) + if operator: operator.report({'INFO'}, f"G-code saved to {filepath}") + return 'ok' + except Exception as e: + if operator: operator.report({'ERROR'}, str(e)) + return 'error' @register_class class SLICENODE_GcodeToTextBlock(ExecSliceNode): @@ -1328,6 +1323,7 @@ class SLICENODE_PrusaslicerSlice(SliceNode): self.add_output("SLICESOCK_GcodeRo") def draw_buttons(self, context, layout): + assert layout layout.prop(self, "prusaslicer_executable") def evaluate_output(self, socket_name: str, **kwargs) -> Any: diff --git a/core/nodeslicer/sockets.py b/core/nodeslicer/sockets.py index 68ab4e6..dde5343 100644 --- a/core/nodeslicer/sockets.py +++ b/core/nodeslicer/sockets.py @@ -88,6 +88,13 @@ class SLICESOCK_FilePath(InputNodeSocket): value: NfStringProperty(name="Filepath", default="", subtype='FILE_PATH') # pyright: ignore[reportInvalidTypeForm] bl_color = (0.4, 0.7, 1.0, 1.0) +@register_class +class SLICESOCK_DirPath(InputNodeSocket): + bl_label = "Filepath" + + value: NfStringProperty(name="Folder", default="", subtype='DIR_PATH') # pyright: ignore[reportInvalidTypeForm] + bl_color = (0.4, 0.7, 1.0, 1.0) + @register_class class SLICESOCK_VectorRotation_Common(InputNodeSocket): bl_label = "VectorRotation" diff --git a/infra/blender/blender_slice_nodes_generic.py b/infra/blender/blender_slice_nodes_generic.py index 4ed15cf..3fa4791 100644 --- a/infra/blender/blender_slice_nodes_generic.py +++ b/infra/blender/blender_slice_nodes_generic.py @@ -1,4 +1,5 @@ from __future__ import annotations +from dataclasses import field import typing import bpy from contextlib import contextmanager @@ -23,24 +24,61 @@ class SliceNodeSocket(NodeSocket, AutoBlIdName): def draw_color(self, context, node) -> tuple[float, float, float, float]: # pyright: ignore[reportIncompatibleMethodOverride] return self.bl_color +from ...registry import get_label_by_name class SliceNode(Node, AutoBlIdName): + def update(self) -> None: + self._update_multi_inputs() + def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ... def resolve(self, fieldname) -> Any: return resolve_input(self.inputs.get(fieldname)) - def add_output(self, bl_idname: str, name: str = ""): - from ...registry import get_label_by_name - return self.outputs.new(bl_idname, name or get_label_by_name(bl_idname)) + def add_output(self, bl_idname: str, name: str = "", identifier: str = ""): + eval_name = name or get_label_by_name(bl_idname) + return self.outputs.new(bl_idname, eval_name, identifier=identifier or eval_name) - def add_input(self, bl_idname: str, name: str = "", multi: bool = False): - from ...registry import get_label_by_name - sock = self.inputs.new(bl_idname, name or get_label_by_name(bl_idname)) - if multi: - sock.link_limit = 0 - sock.display_shape = 'LIST' - return sock + def add_input(self, bl_idname: str, name: str = "", identifier: str = ""): + eval_name = name or get_label_by_name(bl_idname) + return self.inputs.new(bl_idname, eval_name, identifier=identifier or eval_name) + def add_multi_input(self, bl_idname: str, name: str = "") -> None: + if not self.get('multi_sockets'): self['multi_sockets'] = {} + if not self['multi_sockets'].get(name): self['multi_sockets'][name] = {'bl_idname': bl_idname, 'name': name or get_label_by_name(bl_idname)} + self._update_multi_inputs() + + def resolve_multi_input(self, fieldname) -> Any: + inputs = [i for i in list(self.inputs) if i.name == fieldname] + return [resolve_input(r) for r in inputs] + + def _update_multi_inputs(self): + if not self.get('multi_sockets'): return + for k, ms in self['multi_sockets'].items(): + def is_active(input) -> bool: + a: bool = input.is_linked or getattr(input, 'value', False) + b: bool = getattr(inputs()[-1], 'value', False) + return a or b + + def inputs(): + return [i for i in list(self.inputs) if i.name == ms['name']] + + socket_type = ms['bl_idname'] + name = ms['name'] + + if len(inputs()) == 0: + self.add_input(socket_type, name, name+str(len(inputs()))) + return + + if is_active(inputs()[-1]): + self.add_input(socket_type, name, name+str(len(inputs()))) + + while True: + if len(inputs()) < 2: break + if not is_active(inputs()[-1]) and not is_active(inputs()[-2]): + self.inputs.remove(inputs()[-1]) + continue + break + ## Evaluation helpers @contextmanager diff --git a/registry.py b/registry.py index b0db828..9c87003 100644 --- a/registry.py +++ b/registry.py @@ -14,11 +14,11 @@ def register_class(cls: type) -> type: def get() -> list[type]: return list(_bpy_class_registry) -def get_label_by_name(name: str) -> str | None: +def get_label_by_name(name: str) -> str: for cls in get(): if cls.__name__ == name: return getattr(cls, "bl_label", "No Label") - return None + return '' def get_class_by_name(name: str) -> type | None: for cls in get():