from __future__ import annotations from numpy._typing._array_like import NDArray from numpy import float64 bl_info = { "name": "NodeSlicer", "author": "OpenAI", "version": (0, 1, 0), "blender": (3, 6, 0), "location": "Node Editor > Add", "description": "Very small custom nodes example", "category": "Node", } from math import floor from typing import TYPE_CHECKING, Any import bpy import os import numpy as np from bpy.types import NodeTree, Operator, Menu from bpy.props import PointerProperty, StringProperty, FloatProperty, IntProperty from .registry import register_class from .core import slice from .core.slice import Pather, slice_at_range from .infra.blender_mesh_capture import SlicingObject, capture_mesh_edges from .infra.blender_slice_nodes_generic import SliceNode, SliceNodeSocket, resolve_input if TYPE_CHECKING: from bpy.stub_internal.rna_enums import OperatorReturnItems # ----------------------------------------------------------------------------- # Node tree # ----------------------------------------------------------------------------- @register_class class SLICETREE_NodeTree(NodeTree): bl_idname = "SLICETREE_NodeTree" bl_label = "NodeSlicer Tree" bl_icon = "NODETREE" # ----------------------------------------------------------------------------- # Sockets # ----------------------------------------------------------------------------- class LabelNodeSocket(SliceNodeSocket): default = "" def draw(self, context: Any, layout: Any, node: Any, text: str | None): assert layout layout.label(text=text or self.name) class InputNodeSocket(SliceNodeSocket): def draw(self, context: Any, layout: Any, node: Any, text: str | None): assert layout if not self.is_output and self.is_linked: layout.label(text=text or self.name) else: layout.prop(self, "value", text=text or self.name) def _draw_as_ro(self, context: Any, layout: Any, node: Any, text: str | None): layout.label(text=text or self.name) def _draw_as_rw(self, context: Any, layout: Any, node: Any, text: str | None): layout.prop(self, "value", text=text or self.name) class ReadonlySocket(): def draw(self, context: Any, layout: Any, node: Any, text: str | None): return self._draw_as_ro(context, layout, node, text) # pyright: ignore[reportAttributeAccessIssue] @register_class class SLICESOCK_Object(InputNodeSocket): bl_label = "Object" value: PointerProperty( name="Object", type=bpy.types.Object, ) # pyright: ignore[reportInvalidTypeForm] color = (0.9, 0.7, 0.2, 1.0) class SLICESOCK_Float_Common(InputNodeSocket): bl_label = "Float" value: FloatProperty(name="Float", default=0) # pyright: ignore[reportInvalidTypeForm] color = (161/255, 161/255, 161/255, 1.0) @register_class class SLICESOCK_FloatRo(ReadonlySocket, SLICESOCK_Float_Common): ... @register_class class SLICESOCK_Float(SLICESOCK_Float_Common): ... class SLICESOCK_Int_Common(InputNodeSocket): bl_label = "Integer" value: IntProperty(name="Int", default=0) # pyright: ignore[reportInvalidTypeForm] color = (161/255, 161/255, 161/255, 1.0) @register_class class SLICESOCK_Int(SLICESOCK_Int_Common): ... @register_class class SLICESOCK_IntRo(ReadonlySocket, SLICESOCK_Int_Common): ... @register_class class SLICESOCK_String(InputNodeSocket): bl_label = "String" value: StringProperty(name="String", default="") # pyright: ignore[reportInvalidTypeForm] color = (0.4, 0.7, 1.0, 1.0) @register_class class SLICESOCK_Mesh(LabelNodeSocket): bl_label = "Mesh" default = None color = (0, 214/255, 163/255, 1.0) @register_class class SLICESOCK_Slices(LabelNodeSocket): bl_label = "Slices" default = [] color = (0, 214/255, 96/255, 1.0) @register_class class SLICESOCK_Paths(LabelNodeSocket): bl_label = "Paths" default = [] color = (214/255, 0, 120/255, 1.0) @register_class class SLICESOCK_Gcode(LabelNodeSocket): bl_label = "Gcode" default = "" color = (1., 1., 1., 1.) # ----------------------------------------------------------------------------- # Nodes # ----------------------------------------------------------------------------- # Basic Inputs class InputNode(SliceNode): input_type: str = "" def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) if "bl_label" not in cls.__dict__: cls.bl_label = cls.input_type def init(self, context) -> None: self.add_output(f"SLICESOCK_{self.bl_label}") def evaluate_output(self, socket_name: str, **_kwargs): return self.outputs.get(socket_name).value @register_class class SLICENODE_InputString(InputNode): input_type = "String" @register_class class SLICENODE_InputFloat(InputNode): input_type = "Float" @register_class class SLICENODE_InputInt(InputNode): input_type = "Int" @register_class class SLICENODE_InputObject(InputNode): input_type = "Object" bl_icon = "OBJECT_DATA" # Math @register_class class SLICENODE_IntMath(SliceNode): bl_label = "Integer Math" operation: bpy.props.EnumProperty( name="Operation", items=[ ('ADD', "Add", "Add the two values"), ('SUBTRACT', "Subtract", "Subtract the second value from the first"), ('MULTIPLY', "Multiply", "Multiply the two values"), ('DIVIDE', "Divide", "Integer division"), ('ABS', "Absolute", "Absolute value of the first value"), ('NEGATE', "Negate", "Negate the first value"), ('POWER', "Power", "First value raised to the power of the second"), ('MIN', "Minimum", "The smaller of the two values"), ('MAX', "Maximum", "The larger of the two values"), ('SIGN', "Sign", "The sign of the first value (-1, 0, or 1)"), ('DIVIDE_ROUND', "Divide Round", "Division rounded to the nearest integer"), ('DIVIDE_FLOOR', "Divide Floor", "Division rounded down"), ('DIVIDE_TRUNCATE', "Divide Truncate", "Division rounded towards zero"), ('MODULO_FLOORED', "Floored Modulo", "Modulo using floor division"), ('MODULO_TRUNCATED', "Truncated Modulo", "Modulo using truncated division"), ], default='ADD' ) # pyright: ignore[reportInvalidTypeForm] def init(self, context): self.add_output("SLICESOCK_IntRo", "Value") self.add_input("SLICESOCK_Int", "Value") self.add_input("SLICESOCK_Int", "Value") def draw_buttons(self, context, layout): assert layout layout.prop(self, "operation", text="") def evaluate_output(self, socket_name: str, **_kwargs) -> int: a: int = resolve_input(self.inputs[0]) b: int = resolve_input(self.inputs[1]) op = self.operation try: if op == 'ADD': return a + b if op == 'SUBTRACT': return a - b if op == 'MULTIPLY': return a * b if op == 'DIVIDE': return a // b if b != 0 else 0 if op == 'ABS': return abs(a) if op == 'NEGATE': return -a if op == 'POWER': return int(a ** b) if not (a == 0 and b < 0) else 0 if op == 'MIN': return min(a, b) if op == 'MAX': return max(a, b) if op == 'SIGN': return (a > 0) - (a < 0) if op == 'DIVIDE_ROUND': return int(round(a / b)) if b != 0 else 0 if op == 'DIVIDE_FLOOR': return a // b if b != 0 else 0 if op == 'DIVIDE_TRUNCATE': return int(a / b) if b != 0 else 0 if op == 'MODULO_FLOORED': return a % b if b != 0 else 0 if op == 'MODULO_TRUNCATED': return a - (b * int(a / b)) if b != 0 else 0 except Exception: return 0 return 0 @register_class class SLICENODE_GetMesh(SliceNode): bl_label = "Get Mesh" def init(self, context): self.add_input("SLICESOCK_Object") self.add_output("SLICESOCK_Mesh") def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject | None: if socket_name != "Mesh": return None obj: Any = resolve_input(self.inputs.get("Object")) so = SlicingObject(obj, obj.name) return so @register_class class SLICENODE_GetPaths(SliceNode): bl_label = "Get Paths" def init(self, context): self.add_input("SLICESOCK_Object") self.add_output("SLICESOCK_Paths") def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]: obj: Any = resolve_input(self.inputs.get("Object")) so: list[NDArray[float64]] = capture_mesh_edges(obj) return so @register_class class SLICENODE_SliceShells(SliceNode): bl_label = "Slice Shells" def init(self, context): self.add_input("SLICESOCK_Mesh") self.add_input("SLICESOCK_Float", "Layer Height").value = 0.2 self.add_output("SLICESOCK_Slices") def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]: if socket_name != "Slices": return [] slicingmesh: SlicingObject | None = resolve_input(self.inputs.get("Mesh")) layer_height: float = resolve_input(self.inputs.get("Layer Height")) if not slicingmesh: return [] min = slicingmesh.mesh[:,0:3,2:3].min() min = floor(min / layer_height) * layer_height max = slicingmesh.mesh[:,0:3,2:3].max() z_values = np.arange(min, max + layer_height, layer_height) slices_group: list[list[np.ndarray]] | None = slice_at_range([slicingmesh.mesh], z_values) assert slices_group slices = slices_group[0] return slices @register_class class SLICENODE_BufferShells(SliceNode): bl_label = "Buffer Shells" shape_option: bpy.props.EnumProperty( name="Miter", items=[ ('round', "Round", ""), ('square', "Square", ""), ('flat', "Flat", "") ] ) # pyright: ignore[reportInvalidTypeForm] def init(self, context): self.add_input("SLICESOCK_Slices") self.add_input("SLICESOCK_Float", "Distance").value = 0 self.add_output("SLICESOCK_Slices") def draw_buttons(self, context, layout): assert layout layout.prop(self, "shape_option") def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]: if socket_name != "Slices": return [] slices: list[np.ndarray] = resolve_input(self.inputs.get("Slices")) distance: float = resolve_input(self.inputs.get("Distance")) buffered_slices = slice.buffer_slice(slices, distance, miter=self.shape_option) return buffered_slices @register_class class SLICENODE_SlicesToPaths(SliceNode): bl_label = "Slices to Paths" def init(self, context): self.add_input("SLICESOCK_Slices") self.add_output("SLICESOCK_Paths") def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]: slices: list[np.ndarray] = resolve_input(self.inputs.get("Slices")) return slices @register_class class SLICENODE_PathsToGcode(SliceNode): bl_label = "Paths to Gcode" def init(self, context): self.add_input("SLICESOCK_Paths") self.add_output("SLICESOCK_Gcode") def evaluate_output(self, socket_name: str, **_kwargs) -> str: paths: list[np.ndarray] = resolve_input(self.inputs.get("Paths")) pather: Pather = Pather(paths) gcode: str = pather.to_gcode() return gcode @register_class class SLICENODE_GcodeBlock(SliceNode): bl_label = "Gcode Block" gcode: StringProperty(name="Gcode", default="") # pyright: ignore[reportInvalidTypeForm] def init(self, context): self.add_output("SLICESOCK_Gcode") def draw_buttons(self, context, layout): assert layout layout.prop(self, "gcode", text="") def evaluate_output(self, socket_name: str, **_kwargs) -> str: return self.gcode ### Joiners class SliceNodeJoiner(SliceNode): socket_type: str = "" output_socket_type: str = "" def init(self, context): self.add_output(self.output_socket_type or self.socket_type) def update(self) -> None: def is_active(input) -> bool: return input.is_linked or getattr(input, 'value', False) 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 @register_class class SLICENODE_JoinGcode(SliceNodeJoiner): bl_label = "Join Gcode" socket_type = "SLICESOCK_Gcode" def evaluate_output(self, socket_name: str, **_kwargs) -> str: gcode_input: list[str] = [] for input in self.inputs: gcode_input += [resolve_input(input)] or [""] return "\n".join(gcode_input) @register_class class SLICENODE_JoinSlices(SliceNodeJoiner): bl_label = "Join Slices" socket_type = "SLICESOCK_Slices" operation: bpy.props.EnumProperty( name="Operation", items=[ ('UNION', "Union", "Union of the slices"), ('INTERSECTION', "Intersection", "Intersection of the slices"), ('DIFFERENCE', "Subtract", "Clipping of the first slice"), ('XOR', "Exclusive or", "Exclusive or of the slices"), ], default='UNION' ) # pyright: ignore[reportInvalidTypeForm] def draw_buttons(self, context, layout): assert layout layout.prop(self, "operation", text="") def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]: accumulated: list[np.ndarray] = [] current: list[np.ndarray] = [] for input in self.inputs: current =resolve_input(input) if not current: continue if not accumulated: accumulated = current current = [] continue accumulated = slice.boolean_slice(accumulated, current, self.operation) current = [] return accumulated @register_class class SLICENODE_JoinMesh(SliceNodeJoiner): bl_label = "Join Mesh" socket_type = "SLICESOCK_Mesh" def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject: merged_mesh = SlicingObject() for input in self.inputs: mesh =resolve_input(input) if mesh: merged_mesh.mesh = np.concatenate([merged_mesh.mesh, mesh.mesh], axis=0) return merged_mesh @register_class class SLICENODE_JoinPaths(SliceNodeJoiner): bl_label = "Join Paths" socket_type = "SLICESOCK_Paths" def evaluate_output(self, socket_name: str, **_kwargs) -> list[np.ndarray]: merged_paths = [] for input in self.inputs: paths = resolve_input(input) if paths: merged_paths.extend(paths) return merged_paths ### I/O @register_class class SLICENODE_LoadGcode(SliceNode): bl_label = "Load Gcode File" def init(self, context): self.add_input("SLICESOCK_String", "Filepath") self.add_output("SLICESOCK_Gcode") def evaluate_output(self, socket_name: str, **_kwargs) -> str: path = resolve_input(self.inputs.get("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 "" class ExecSliceNode(SliceNode): button_label: str = 'Execute' def draw_buttons(self, context, layout): assert layout layout.operator("slicenode.execute", text=self.button_label).node_name = self.name @register_class class SLICENODE_GcodeToFile(ExecSliceNode): bl_label = "Gcode To File" button_label = "Generate File" def init(self, context): self.add_input("SLICESOCK_Gcode") self.add_input("SLICESOCK_String", "Filename") def evaluate(self): gcode: str = resolve_input(self.inputs.get("Gcode")) filename: str = resolve_input(self.inputs.get("Filename")) filepath = bpy.path.abspath(filename) os.makedirs(os.path.dirname(filepath) or ".", exist_ok=True) with open(filepath, "w", encoding="utf-8", newline="\n") as f: f.write(gcode) return 'ok' @register_class class SLICENODE_GcodeToMesh(ExecSliceNode): bl_label = "Gcode to Mesh" button_label = "Update Debug Mesh" def init(self, context): self.add_input("SLICESOCK_Gcode") def evaluate(self): gcode: str = resolve_input(self.inputs.get("Gcode")) if not gcode: return 'empty' segments = [] last_x, last_y, last_z = 0.0, 0.0, 0.0 last_e = 0.0 for line in gcode.splitlines(): line = line.split(';')[0].strip() if not line: continue parts = line.split() cmd = parts[0].upper() params = {} for p in parts[1:]: if len(p) < 2: continue try: # Scale coordinates by 0.001 (mm to Blender units) val = float(p[1:]) if p[0].upper() in 'XYZ': val *= 0.001 params[p[0].upper()] = val except ValueError: continue new_x = params.get('X', last_x) new_y = params.get('Y', last_y) new_z = params.get('Z', last_z) new_e = params.get('E', last_e) if cmd == 'G1': is_extruding = new_e > 0 if is_extruding: segments.append(((last_x, last_y, last_z), (new_x, new_y, new_z))) last_x, last_y, last_z = new_x, new_y, new_z last_e = new_e if not segments: return 'no segments' # Create or find "Debug" mesh object mesh_name = "Debug" if mesh_name in bpy.data.objects: obj = bpy.data.objects[mesh_name] mesh = obj.data mesh.clear_geometry() else: mesh = bpy.data.meshes.new(mesh_name) obj = bpy.data.objects.new(mesh_name, mesh) assert bpy.context.collection bpy.context.collection.objects.link(obj) vertices = [] edges = [] for i, (p1, p2) in enumerate(segments): vertices.extend([p1, p2]) edges.append((2*i, 2*i + 1)) mesh.from_pydata(vertices, edges, []) mesh.update() return 'ok' @register_class class SLICENODE_Test(ExecSliceNode): bl_label = "Test" button_label = "Test" def init(self, context): self.add_input("SLICESOCK_Gcode") def evaluate(self): _ = resolve_input(self.inputs.get("Gcode")) return 'ok' # ----------------------------------------------------------------------------- # Operator # ----------------------------------------------------------------------------- @register_class class SLICENODE_OT_Execute(Operator): bl_idname = "slicenode.execute" bl_label = "Execute" bl_description = "Execute" node_name: bpy.props.StringProperty() # pyright: ignore[reportInvalidTypeForm] def execute(self, context) -> set['OperatorReturnItems']: node_tree = context.space_data.edit_tree # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess] node = node_tree.nodes[self.node_name] _ = node.evaluate() self.report({"INFO"}, f"Execution Successful") return {"FINISHED"} # ----------------------------------------------------------------------------- # Menu # ----------------------------------------------------------------------------- @register_class class SLICENODE_MT_Menu(Menu): bl_idname = "SLICENODE_MT_Menu" bl_label = "SliceNodes" def draw(self, context): layout = self.layout assert layout layout.operator("node.add_node", text="String").type = "SLICENODE_InputString" layout.operator("node.add_node", text="Float").type = "SLICENODE_InputFloat" layout.operator("node.add_node", text="Integer").type = "SLICENODE_InputInt" layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath" layout.operator("node.add_node", text="Object").type = "SLICENODE_InputObject" layout.operator("node.add_node", text="Test").type = "SLICENODE_Test" layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh" layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths" layout.separator() layout.operator("node.add_node", text="Slice Shells").type = "SLICENODE_SliceShells" layout.operator("node.add_node", text="Buffer Shells").type = "SLICENODE_BufferShells" layout.operator("node.add_node", text="Slices to Paths").type = "SLICENODE_SlicesToPaths" layout.operator("node.add_node", text="Paths to Gcode").type = "SLICENODE_PathsToGcode" layout.separator() layout.operator("node.add_node", text="Gcode Block").type = "SLICENODE_GcodeBlock" layout.operator("node.add_node", text="Load Gcode").type = "SLICENODE_LoadGcode" layout.separator() layout.operator("node.add_node", text="Join Gcode").type = "SLICENODE_JoinGcode" layout.operator("node.add_node", text="Join Mesh").type = "SLICENODE_JoinMesh" layout.operator("node.add_node", text="Join Paths").type = "SLICENODE_JoinPaths" layout.operator("node.add_node", text="Join Slices").type = "SLICENODE_JoinSlices" layout.separator() layout.operator("node.add_node", text="Gcode to File").type = "SLICENODE_GcodeToFile" layout.operator("node.add_node", text="Gcode to Mesh").type = "SLICENODE_GcodeToMesh" def draw_add_menu(self, context): space = context.space_data tree = getattr(space, "edit_tree", None) if tree and tree.bl_idname == "SLICETREE_NodeTree": self.layout.separator() self.layout.menu("SLICENODE_MT_Menu") # ----------------------------------------------------------------------------- # Registration # ----------------------------------------------------------------------------- ### Load collected modules modules = registry.get() def register(): registry.blender_register_classes() bpy.types.NODE_MT_add.append(draw_add_menu) print('Nodeslicer Loaded') def unregister(): bpy.types.NODE_MT_add.remove(draw_add_menu) registry.blender_unregister_classes() print('Nodeslicer Unloaded') if __name__ == "__main__": register()