nodeslicer/core/nodeslicer/nodes/gcode.py
2026-10-12 16:23:02 +03:00

268 lines
10 KiB
Python

from __future__ import annotations
from typing import Any
import numpy as np
import bpy
import os
from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfStringProperty
from ....registry import register_class
from .base import SliceNode, SliceIntrinsics, Pather, SliceNodeJoiner, SliceNodeListJoiner
from ....infra.blender.list_utils import eval_element_wise, flatten_list, is_list_data
@register_class
class SLICENODE_GcodesToList(SliceNodeListJoiner):
bl_label = "Gcodes to List"
socket_type = "NodeSocketString"
socket_label = "Gcode"
@register_class
class SLICENODE_PathsToGcode(SliceNode):
bl_label = "Paths to Gcode"
list_socket_mapping = [("Paths", "Gcode")]
use_header: bool = NfBoolProperty(name="Header", default=True)
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_input("SLICESOCK_Intrinsics")
self.add_output("NodeSocketString", name="Gcode")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "use_header")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
paths: Any = self.resolve("Paths")
intrinsics: SliceIntrinsics | None = self.resolve("Intrinsics")
if not paths:
return ""
def _paths_to_gcode(p: Any) -> str:
if not p:
return ""
pather: Pather = Pather(p)
if hasattr(p, "layer_height"):
pather.layer_height = getattr(p, "layer_height")
if hasattr(p, "first_layer_height"):
pather.first_layer_height = getattr(p, "first_layer_height")
if intrinsics:
pather.bed_center_x = intrinsics.bed_x / 2.0
pather.bed_center_y = intrinsics.bed_y / 2.0
pather.feedrate = intrinsics.feedrate
pather.travel_feedrate = intrinsics.travel_feedrate
pather.extrusion_rate = intrinsics.extrusion_rate
pather.retraction = intrinsics.retraction
pather.retraction_feedrate = getattr(intrinsics, "retraction_feedrate", 0.0)
pather.lift = intrinsics.lift
return pather.to_gcode(include_header=self.use_header)
return eval_element_wise(_paths_to_gcode, paths)
@register_class
class SLICENODE_GcodeBlock(SliceNode):
bl_label = "Gcode Block"
def init(self, context):
self.add_output("NodeSocketString", "Gcode")
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
return self.outputs['Gcode'].value
@register_class
class SLICENODE_AssignMetadata(SliceNode):
bl_label = "Assign Metadata"
def init(self, context):
self.add_input("NodeSocketString", "Name")
self.add_input("NodeSocketString", "Filament Type")
self.add_input("NodeSocketString", "Printer Model")
self.add_output("NodeSocketString", "Gcode")
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
name = self.resolve("Name")
filament = self.resolve("Filament Type")
printer = self.resolve("Printer Model")
lines = []
if name:
lines.append(f'; objects_info = {{"objects": [{{"name": "{name}", "polygon": []}}]}}')
if filament:
lines.append(f'; filament_type = {filament}')
if printer:
lines.append(f'; printer_model = {printer}')
return '\n'.join(lines)
@register_class
class SLICENODE_GcodeRegexReplace(SliceNode):
bl_label = "Gcode Regex Replace"
list_socket_mapping = [("Gcode", "Gcode")]
def init(self, context):
self.add_input("NodeSocketString", "Gcode")
self.add_input("NodeSocketString", "Find")
self.add_input("NodeSocketString", "Replace")
self.add_output("NodeSocketString", "Gcode")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
import re
gcode = self.resolve("Gcode")
find = self.resolve("Find") or ""
replace = self.resolve("Replace") or ""
if not gcode:
return ""
if not find:
return gcode
def _replace_one(gc: Any) -> str:
gc_str = str(gc or "")
if not find or not gc_str:
return gc_str
try:
# Convert $1, $2, ${name} etc to Python backreferences \g<1>, \g<2>, \g<name>
def subst(match):
if match.group(1): return '$'
elif match.group(2): return '\\g<' + match.group(2) + '>'
elif match.group(3): return '\\g<' + match.group(3) + '>'
return match.group(0)
rep = re.sub(r'(\$\$)|(?:\$\{([^}]+)\})|(?:\$([a-zA-Z0-9_]+))', subst, replace)
return re.sub(find, rep, gc_str)
except Exception as e:
print(f"Regex error in node {self.name}: {e}")
return gc_str
return eval_element_wise(_replace_one, gcode)
@register_class
class SLICENODE_GcodeHelper(SliceNode):
bl_label = "Gcode Helper"
command: str = NfEnumProperty(
name="Command",
items=[
('G0', "G0: Rapid Move", "Fast movement without extrusion"),
('G1', "G1: Linear Move", "Movement with extrusion"),
('G4', "G4: Dwell", "Wait for time (e.g. P500 for 500ms or S5 for 5s)"),
('G28', "G28: Home All", "Home all axes"),
('G90', "G90: Absolute Pos", "Set to absolute positioning"),
('G91', "G91: Relative Pos", "Set to relative positioning"),
('G92', "G92: Set Position", "Set the current position to specific values (e.g. G92 E0)"),
('M0', "M0: Stop/Wait", "Stop and wait for user button press"),
('M25', "M25: Pause SD", "Pause SD print (Marlin/RepRap)"),
('M104', "M104: Set Temp", "Set hotend temperature"),
('M109', "M109: Wait Temp", "Set hotend temperature and wait"),
('M140', "M140: Set Bed Temp", "Set bed temperature"),
('M190', "M190: Wait Bed Temp", "Set bed temperature and wait"),
('M106', "M106: Fan On", "Turn on cooling fan"),
('M107', "M107: Fan Off", "Turn off cooling fan"),
('M117', "M117: Message", "Display message on printer LCD"),
('M220', "M220: Speed Factor", "Set speed percentage (e.g. S110 for 110%)"),
('M221', "M221: Flow Factor", "Set flow percentage (e.g. S95 for 95%)"),
('M300', "M300: Beep", "Play beep sound"),
('M400', "M400: Wait Finish", "Wait for all current moves to finish"),
('M600', "M600: Color Change", "Filament change / Color change"),
('M82', "M82: Abs Extrusion", "Set extrusion to absolute mode"),
('M83', "M83: Rel Extrusion", "Set extrusion to relative mode"),
('PAUSE', "PAUSE: Klipper Pause", "Pause print (Klipper macro)"),
('T', "T: Change Extruder", "Switch to a different extruder (e.g. T0, T1)"),
],
default='G1'
)
parameters: str = NfStringProperty(name="Params", default="")
def init(self, context):
self.add_output("NodeSocketString")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "command", text="")
# Commands that don't need parameters
no_params = {'G28', 'G90', 'G91', 'M0', 'M25', 'M107', 'M400', 'M600', 'M82', 'M83', 'PAUSE'}
if self.command not in no_params:
layout.prop(self, "parameters", text="Parameters")
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
cmd = self.command
params = self.parameters.strip()
if params:
# T commands usually don't have a space (e.g. T0)
if cmd == 'T':
return f"{cmd}{params}"
return f"{cmd} {params}"
return cmd
@register_class
class SLICENODE_JoinGcode(SliceNodeJoiner):
bl_label = "Join Gcode"
socket_type = "NodeSocketString"
output_socket_type = "NodeSocketString"
socket_label = "Gcode"
def update(self) -> None:
super().update()
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
has_list = any(
is_socket_linked_to_list(inp)
for inp in self.inputs
if getattr(inp, "name", "") == self.socket_label or getattr(inp, "identifier", "").startswith(f"{self.socket_label}_ms_in_")
)
out_sock = self.outputs.get(self.socket_label)
if out_sock:
set_socket_shape(out_sock, "LIST" if has_list else "CIRCLE")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
raw_inputs = self.resolve_multi_input(self.socket_label)
has_list = any(is_list_data(inp) for inp in raw_inputs)
if not has_list:
flattened = flatten_list(raw_inputs)
gcode_input: list[str] = [str(gc) for gc in flattened if gc]
return "\n".join(gcode_input)
# There is at least one list input
list_lengths = [len(inp) for inp in raw_inputs if is_list_data(inp)]
if len(set(list_lengths)) > 1:
raise ValueError(
f"Join Gcode: all input lists must have the same length, got lengths {list_lengths}"
)
n_elements = list_lengths[0] if list_lengths else 0
result: list[str] = []
for i in range(n_elements):
row_parts: list[str] = []
for inp in raw_inputs:
if is_list_data(inp):
val = inp[i]
else:
val = inp
if is_list_data(val):
val_str = "\n".join(str(s) for s in flatten_list(val) if s)
if val_str:
row_parts.append(val_str)
elif val:
row_parts.append(str(val))
result.append("\n".join(row_parts))
return result
@register_class
class SLICENODE_LoadGcode(SliceNode):
bl_label = "Load Gcode File"
def init(self, context):
self.add_input("NodeSocketStringFilePath", "Filepath")
self.add_output("NodeSocketString", name= "Gcode")
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
path = self.resolve("Filepath")
if not path: return ""
abs_path = bpy.path.abspath(path)
if os.path.exists(abs_path):
with open(abs_path, 'r', encoding='utf-8') as f:
return f.read()
return ""