diff --git a/core/nodeslicer/nodes.py b/core/nodeslicer/nodes.py index a9c737f..d382c3c 100644 --- a/core/nodeslicer/nodes.py +++ b/core/nodeslicer/nodes.py @@ -621,6 +621,29 @@ class SLICENODE_GcodeBlock(SliceNode): 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("SLICESOCK_String", "Name") + self.add_input("SLICESOCK_String", "Filament Type") + self.add_input("SLICESOCK_String", "Printer Model") + self.add_output("SLICESOCK_GcodeRo", "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_GcodeHelper(SliceNode): bl_label = "Gcode Helper" diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index 29515b2..a895df1 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -194,6 +194,7 @@ class SLICENODE_MT_Gcode(Menu): assert layout layout.operator("node.add_node", text="Paths to Gcode").type = "SLICENODE_PathsToGcode" layout.operator("node.add_node", text="Gcode Block").type = "SLICENODE_GcodeBlock" + layout.operator("node.add_node", text="Assign Metadata").type = "SLICENODE_AssignMetadata" layout.operator("node.add_node", text="Gcode Helper").type = "SLICENODE_GcodeHelper" layout.operator("node.add_node", text="Load Gcode").type = "SLICENODE_LoadGcode" diff --git a/infra/blender/blender_slice_nodes_generic.py b/infra/blender/blender_slice_nodes_generic.py index 3fa4791..11440f5 100644 --- a/infra/blender/blender_slice_nodes_generic.py +++ b/infra/blender/blender_slice_nodes_generic.py @@ -9,7 +9,7 @@ from bpy.types import NodeSocket, Node class NodeException(Exception): ... -_EvalCache = dict[tuple[int, str], Any] +_EvalCache = dict[tuple[int, str, str], Any] class AutoBlIdName: def __init_subclass__(cls, **kwargs): @@ -32,7 +32,9 @@ class SliceNode(Node, AutoBlIdName): def evaluate_output(self, socket_name: str, **_kwargs) -> Any: ... def resolve(self, fieldname) -> Any: - return resolve_input(self.inputs.get(fieldname)) + cache = getattr(self, "_current_cache", None) + visited = getattr(self, "_current_visited", None) + return resolve_input(self.inputs.get(fieldname), cache=cache, visited=visited) def add_output(self, bl_idname: str, name: str = "", identifier: str = ""): eval_name = name or get_label_by_name(bl_idname) @@ -48,8 +50,10 @@ class SliceNode(Node, AutoBlIdName): self._update_multi_inputs() def resolve_multi_input(self, fieldname) -> Any: + cache = getattr(self, "_current_cache", None) + visited = getattr(self, "_current_visited", None) inputs = [i for i in list(self.inputs) if i.name == fieldname] - return [resolve_input(r) for r in inputs] + return [resolve_input(r, cache=cache, visited=visited) for r in inputs] def _update_multi_inputs(self): if not self.get('multi_sockets'): return @@ -81,6 +85,21 @@ class SliceNode(Node, AutoBlIdName): ## Evaluation helpers +_current_execution_cache: _EvalCache | None = None + +@contextmanager +def execution_cache(): + global _current_execution_cache + if _current_execution_cache is not None: + yield _current_execution_cache + return + + _current_execution_cache = {} + try: + yield _current_execution_cache + finally: + _current_execution_cache = None + @contextmanager def node_execution_feedback(node: bpy.types.Node): # Save original color @@ -93,7 +112,8 @@ def node_execution_feedback(node: bpy.types.Node): # Refresh UI bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) try: - yield + with execution_cache(): + yield finally: node.use_custom_color = old_use_color node.color = old_color @@ -129,6 +149,11 @@ def resolve_input( assert socket.node + if cache is None: + cache = getattr(socket.node, "_current_cache", _current_execution_cache) + if visited is None: + visited = getattr(socket.node, "_current_visited", None) + if not socket.is_linked: val = getattr(socket, "value", None) if val is not None: @@ -153,39 +178,51 @@ def _resolve_output( cache: _EvalCache | None = None, visited: set[tuple[int, str]] | None = None, ): + if cache is None: + cache = _current_execution_cache cache = {} if cache is None else cache visited = set() if visited is None else visited assert socket.node - key = (socket.node.as_pointer(), socket.node.name) + key = (socket.node.as_pointer(), socket.node.name, socket.name) if key in cache: return cache[key] - if key in visited: return default # cycle guard - visited.add(key) + node_key = (socket.node.as_pointer(), socket.node.name) + if node_key in visited: return default # cycle guard + + visited.add(node_key) try: with node_execution_feedback(socket.node): if not isinstance(socket.node, SliceNode): return default - if getattr(socket.node, "mute", False): - # Attempt bypass: find a matching input socket by name or type - input_to_bypass = socket.node.inputs.get(socket.name) - if not input_to_bypass: - for inp in socket.node.inputs: - if inp.bl_idname == socket.bl_idname: - input_to_bypass = inp - break - - if input_to_bypass: - value = resolve_input(typing.cast(Any, input_to_bypass), cache=cache, visited=visited) - cache[key] = value - return value - return default + # Set these temporarily so self.resolve() can find them + socket.node._current_cache = cache # type: ignore + socket.node._current_visited = visited # type: ignore - assert socket.node.evaluate_output - value = socket.node.evaluate_output(socket.name, cache=cache, visited=visited) - cache[key] = value - return value + try: + if getattr(socket.node, "mute", False): + # Attempt bypass: find a matching input socket by name or type + input_to_bypass = socket.node.inputs.get(socket.name) + if not input_to_bypass: + for inp in socket.node.inputs: + if inp.bl_idname == socket.bl_idname: + input_to_bypass = inp + break + + if input_to_bypass: + value = resolve_input(typing.cast(Any, input_to_bypass), cache=cache, visited=visited) + cache[key] = value + return value + return default + + assert socket.node.evaluate_output + value = socket.node.evaluate_output(socket.name, cache=cache, visited=visited) + cache[key] = value + return value + finally: + if hasattr(socket.node, "_current_cache"): del socket.node._current_cache + if hasattr(socket.node, "_current_visited"): del socket.node._current_visited finally: - visited.remove(key) + visited.remove(node_key) diff --git a/infra/networking/gcode_utils.py b/infra/networking/gcode_utils.py index 2c182d9..4d32205 100644 --- a/infra/networking/gcode_utils.py +++ b/infra/networking/gcode_utils.py @@ -52,7 +52,8 @@ def get_gcode_name_from_metadata(gcode: str, default: str = "output") -> str: names: list[str] = [str(v['name'].split(" id:")[0]) for v in gcode_info['objects']] counts = Counter(names) object_names = "-".join(f"{c}x{n}" if c > 1 else n for n, c in counts.items()) - return "-".join([object_names, filament, printer]) + parts = [object_names, filament, printer] + return "-".join([str(p) for p in parts if p]) except: pass return default