From fd891796348ae93aa8b0c1da8067c01b9bb37720 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Sun, 17 May 2026 10:35:22 +0300 Subject: [PATCH] object properties --- core/nodeslicer/nodes.py | 113 +++++++++++++++++++++------ core/nodeslicer/tree.py | 3 +- infra/blender_mesh_capture.py | 22 +++++- infra/blender_slice_nodes_generic.py | 4 +- 4 files changed, 112 insertions(+), 30 deletions(-) diff --git a/core/nodeslicer/nodes.py b/core/nodeslicer/nodes.py index 76256b1..6e12c97 100644 --- a/core/nodeslicer/nodes.py +++ b/core/nodeslicer/nodes.py @@ -15,7 +15,7 @@ from .bpy_ns_types import NfStringProperty, NfEnumProperty, NfBoolProperty from ...registry import register_class from .. import slice from ..slice import Pather, slice_at_range, SlicePaths -from ...infra.blender_mesh_capture import SlicingObject, capture_mesh_edges, SlicingGroup +from ...infra.blender_mesh_capture import SlicingCollection, SlicingObject, capture_mesh_edges, SlicingGroup from ...infra.blender_slice_nodes_generic import SliceNode, resolve_input from .bpy_ns_types import notify_tree_changed from ...infra._3mf_writer._3mf import prepare_3mf @@ -376,18 +376,45 @@ class SLICENODE_IntMath(MathNode): class SLICENODE_GetMesh(SliceNode): bl_label = "Get Mesh" + part_type: NfEnumProperty( # pyright: ignore[reportInvalidTypeForm] + name='', + items=[ + ('ModelPart', 'Model Part', ''), + ('NegativeVolume', 'Negative Volume', ''), + ('ParameterModifier', 'Parameter Modifier', ''), + ('SupportBlocker', 'Support Blocker', ''), + ('SupportEnforcer', 'Support Enforcer', '') + ], + default='ModelPart' + ) + def init(self, context): self.add_input("SLICESOCK_Object") + self.add_input("SLICESOCK_Overrides") + self.add_input("SLICESOCK_Int", "Extruder").value = 1 self.add_output("SLICESOCK_Mesh") - def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject | None: + def draw_buttons(self, context, layout): + assert layout + layout.prop(self, "part_type") + + def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | None: if socket_name != "Mesh": return None - obj: Any = resolve_input(self.inputs.get("Object")) + obj: Any = self.resolve("Object") + modifiers: dict[str, str] = self.resolve("Overrides") + extruder = self.resolve("Extruder") + part_type = self.part_type - so = SlicingObject(obj, obj.name) + so: SlicingObject = SlicingObject(obj, obj.name) + so.assign_modifiers(modifiers) + so.object_type = part_type + so.extruder = extruder - return so + sc: SlicingCollection = SlicingCollection([], obj.name) + sc.objects = [so] + + return sc @register_class class SLICENODE_GetPaths(SliceNode): @@ -416,18 +443,24 @@ class SLICENODE_SliceShells(SliceNode): def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths: if socket_name != "Paths": return SlicePaths() - slicingmesh: SlicingObject | None = resolve_input(self.inputs.get("Mesh")) + slicing_collection: SlicingCollection | None = resolve_input(self.inputs.get("Mesh")) layer_height: float = resolve_input(self.inputs.get("Layer Height")) - if not slicingmesh: return SlicePaths() + if not slicing_collection: return SlicePaths() - min = slicingmesh.mesh[:,0:3,2:3].min() + merged_object = SlicingObject() + coll: SlicingCollection = self.resolve("Mesh") + if coll: + for obj in coll.objects: + merged_object.mesh = np.concatenate([merged_object.mesh, obj.mesh], axis=0) + + min = merged_object.mesh[:,0:3,2:3].min() min = floor(min / layer_height) * layer_height - max = slicingmesh.mesh[:,0:3,2:3].max() + max = merged_object.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) + slices_group: list[list[np.ndarray]] | None = slice_at_range([merged_object.mesh], z_values) assert slices_group slices = slices_group[0] @@ -809,18 +842,34 @@ class SLICENODE_JoinSlices(SliceNodeJoiner): @register_class class SLICENODE_JoinMesh(SliceNodeJoiner): - bl_label = "Join Mesh" + bl_label = "Join Mesh Parts" socket_type = "SLICESOCK_Mesh" - def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject: - merged_mesh = SlicingObject() + def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection: + coll: SlicingCollection + merged_coll = resolve_input(self.inputs[0]) + + for input in self.inputs[1:]: + coll = resolve_input(input) + if coll: + merged_coll.objects += coll.objects + return merged_coll + +@register_class +class SLICENODE_JoinMeshGroups(SliceNodeJoiner): + bl_label = "Join Mesh Groups" + socket_type = "SLICESOCK_Mesh" + + def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup: + group: SlicingGroup = SlicingGroup([]) + for input in self.inputs: - mesh =resolve_input(input) - if mesh: - merged_mesh.mesh = np.concatenate([merged_mesh.mesh, mesh.mesh], axis=0) + coll = resolve_input(input) + if coll: + group.collections[coll.objects[0].name] = coll - return merged_mesh + return group @register_class class SLICENODE_JoinPaths(SliceNodeJoiner): @@ -1120,7 +1169,7 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode): def init(self, context): self._init_exec() - self.add_input("SLICESOCK_Object", "Object") + self.add_input("SLICESOCK_Mesh", "Mesh") self.add_input("SLICESOCK_PrusaslicerConf", "Config") self.add_input("SLICESOCK_Overrides", "Overrides") self.add_output("SLICESOCK_GcodeRo") @@ -1135,12 +1184,14 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode): return super().evaluate_output(socket_name, **kwargs) def evaluate(self, operator=None): - obj = resolve_input(self.inputs.get("Object")) + self.gcode_result = "" + mesh_data: SlicingCollection | SlicingGroup = resolve_input(self.inputs.get("Mesh")) + conf_data = resolve_input(self.inputs.get("Config")) overrides = resolve_input(self.inputs.get("Overrides")) - if not obj: - if operator: operator.report({'ERROR'}, "No Object connected") + if not mesh_data: + if operator: operator.report({'ERROR'}, "No Mesh data connected") return 'no object' if not conf_data: if operator: operator.report({'ERROR'}, "No Config connected") @@ -1153,8 +1204,12 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode): cache = conf_data['cache'] # Generate the SlicingGroup - slicing_group = SlicingGroup([obj]) - + if isinstance(mesh_data, SlicingGroup): + slicing_group = mesh_data + else: + slicing_group = SlicingGroup([]) + slicing_group.collections = {mesh_data.objects[0].name: mesh_data} + # Offset by the bed center def parse_bed_center(bed_shape_str: str) -> np.ndarray: try: @@ -1189,7 +1244,7 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode): # Prepare 3mf path temp_dir = Path(tempfile.gettempdir()) - output_path = temp_dir / f"{obj.name}_NodeSlicer.3mf" + output_path = temp_dir / f"tmp.3mf" try: prepare_3mf(output_path, slicing_group, conf_writer, z_gcodes=[]) @@ -1215,6 +1270,7 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode): # Final check if command exists if not shutil.which(cmd_parts[0]): if operator: operator.report({'ERROR'}, f"Command or executable not found: {cmd_parts[0]}") + self.gcode_result = "" return 'error' gcode_path = output_path.with_suffix(".gcode") @@ -1226,20 +1282,25 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode): process = subprocess.run(cmd, capture_output=True, text=True) if process.returncode != 0: if operator: operator.report({'ERROR'}, f"PrusaSlicer failed: {process.stderr}") + self.gcode_result = "" return 'error' # Read G-code if gcode_path.exists(): with open(gcode_path, 'r', encoding='utf-8') as f: self.gcode_result = f.read() + + output_path.unlink() if operator: operator.report({'INFO'}, f"Slicing complete. G-code generated.") else: if operator: operator.report({'ERROR'}, "G-code file was not generated by PrusaSlicer") + self.gcode_result = "" return 'error' except Exception as e: + self.gcode_result = "" if operator: operator.report({'ERROR'}, f"Failed to slice: {str(e)}") import traceback traceback.print_exc() @@ -1263,6 +1324,10 @@ class SLICENODE_GcodeToMesh(ExecSliceNode): mesh_name: str = self.resolve("Name") if not gcode: + if mesh_name in bpy.data.objects: + obj = bpy.data.objects[mesh_name] + if obj.type == 'MESH': + obj.data.clear_geometry() return 'empty' segments = [] diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index b0ed536..60aeac5 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -163,7 +163,8 @@ class SLICENODE_MT_Menu(Menu): 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 Mesh Parts").type = "SLICENODE_JoinMesh" + layout.operator("node.add_node", text="Join Mesh Groups").type = "SLICENODE_JoinMeshGroups" layout.operator("node.add_node", text="Join Paths").type = "SLICENODE_JoinPaths" layout.operator("node.add_node", text="Join Overrides").type = "SLICENODE_JoinOverrides" layout.operator("node.add_node", text="Join Execution").type = "SLICENODE_JoinExecution" diff --git a/infra/blender_mesh_capture.py b/infra/blender_mesh_capture.py index 95f002d..afc37bf 100644 --- a/infra/blender_mesh_capture.py +++ b/infra/blender_mesh_capture.py @@ -1,5 +1,5 @@ from __future__ import annotations -from bpy.types import FloatAttribute, Object, Mesh +from bpy.types import Object, Mesh import bpy import zlib, struct from functools import cached_property @@ -8,7 +8,7 @@ import numpy as np from numpy import dtype, float64 from numpy.typing import NDArray -from typing import Any, cast +from typing import Any from ..core.geometry import crc32_array from ..infra.blender_bridge import get_all_children @@ -44,7 +44,16 @@ class SlicingObject(): self.mesh = np.empty((0, 4, 3)) def offset(self, offset: NDArray): - self.mesh += offset + self.mesh[:, :3, :] += offset + + def scale(self, factor: float): + self.mesh[:, :3, :] *= factor + + def assign_modifiers(self, modifiers: dict[str, str]): + self.modifiers = [ + {"param_id": k, "param_value": v} + for k, v in modifiers.items() + ] @property def checksum(self) -> int: @@ -99,6 +108,9 @@ class SlicingCollection(): def offset(self, offset: NDArray): for so in self.objects: so.offset(offset) + def scale(self, factor: float): + for so in self.objects: so.scale(factor) + @property def checksum(self) -> int: return crc32_array(np.array([o.checksum for o in self.objects])) @@ -253,6 +265,10 @@ class SlicingGroup(): for k, so in self.collections.items(): so.offset(offset) self.wipe_tower_xy = self.wipe_tower_xy + offset[0:2] + def scale(self, factor: float): + for k, so in self.collections.items(): so.scale(factor) + self.wipe_tower_xy *= factor + @property def checksum(self): buf = bytearray() diff --git a/infra/blender_slice_nodes_generic.py b/infra/blender_slice_nodes_generic.py index 0ae8142..d3048be 100644 --- a/infra/blender_slice_nodes_generic.py +++ b/infra/blender_slice_nodes_generic.py @@ -27,11 +27,11 @@ class SliceNode(Node, AutoBlIdName): def resolve(self, fieldname) -> Any: return resolve_input(self.inputs.get(fieldname)) - def add_output(self, bl_idname: str, name: str | None = None): + 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_input(self, bl_idname: str, name: str | None = None, multi: bool = False): + 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: