268 lines
10 KiB
Python
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 ""
|