object properties

This commit is contained in:
Nicolas Predella 2026-05-17 10:35:22 +03:00
parent a672043385
commit fd89179634
4 changed files with 112 additions and 30 deletions

View file

@ -15,7 +15,7 @@ from .bpy_ns_types import NfStringProperty, NfEnumProperty, NfBoolProperty
from ...registry import register_class from ...registry import register_class
from .. import slice from .. import slice
from ..slice import Pather, slice_at_range, SlicePaths 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 ...infra.blender_slice_nodes_generic import SliceNode, resolve_input
from .bpy_ns_types import notify_tree_changed from .bpy_ns_types import notify_tree_changed
from ...infra._3mf_writer._3mf import prepare_3mf from ...infra._3mf_writer._3mf import prepare_3mf
@ -376,18 +376,45 @@ class SLICENODE_IntMath(MathNode):
class SLICENODE_GetMesh(SliceNode): class SLICENODE_GetMesh(SliceNode):
bl_label = "Get Mesh" 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): def init(self, context):
self.add_input("SLICESOCK_Object") self.add_input("SLICESOCK_Object")
self.add_input("SLICESOCK_Overrides")
self.add_input("SLICESOCK_Int", "Extruder").value = 1
self.add_output("SLICESOCK_Mesh") 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 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 @register_class
class SLICENODE_GetPaths(SliceNode): class SLICENODE_GetPaths(SliceNode):
@ -416,18 +443,24 @@ class SLICENODE_SliceShells(SliceNode):
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths: def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
if socket_name != "Paths": return 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")) 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 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) 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 assert slices_group
slices = slices_group[0] slices = slices_group[0]
@ -809,18 +842,34 @@ class SLICENODE_JoinSlices(SliceNodeJoiner):
@register_class @register_class
class SLICENODE_JoinMesh(SliceNodeJoiner): class SLICENODE_JoinMesh(SliceNodeJoiner):
bl_label = "Join Mesh" bl_label = "Join Mesh Parts"
socket_type = "SLICESOCK_Mesh" socket_type = "SLICESOCK_Mesh"
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingObject: def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection:
merged_mesh = SlicingObject() 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: for input in self.inputs:
mesh =resolve_input(input) coll = resolve_input(input)
if mesh: if coll:
merged_mesh.mesh = np.concatenate([merged_mesh.mesh, mesh.mesh], axis=0) group.collections[coll.objects[0].name] = coll
return merged_mesh return group
@register_class @register_class
class SLICENODE_JoinPaths(SliceNodeJoiner): class SLICENODE_JoinPaths(SliceNodeJoiner):
@ -1120,7 +1169,7 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode):
def init(self, context): def init(self, context):
self._init_exec() 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_PrusaslicerConf", "Config")
self.add_input("SLICESOCK_Overrides", "Overrides") self.add_input("SLICESOCK_Overrides", "Overrides")
self.add_output("SLICESOCK_GcodeRo") self.add_output("SLICESOCK_GcodeRo")
@ -1135,12 +1184,14 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode):
return super().evaluate_output(socket_name, **kwargs) return super().evaluate_output(socket_name, **kwargs)
def evaluate(self, operator=None): 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")) conf_data = resolve_input(self.inputs.get("Config"))
overrides = resolve_input(self.inputs.get("Overrides")) overrides = resolve_input(self.inputs.get("Overrides"))
if not obj: if not mesh_data:
if operator: operator.report({'ERROR'}, "No Object connected") if operator: operator.report({'ERROR'}, "No Mesh data connected")
return 'no object' return 'no object'
if not conf_data: if not conf_data:
if operator: operator.report({'ERROR'}, "No Config connected") if operator: operator.report({'ERROR'}, "No Config connected")
@ -1153,7 +1204,11 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode):
cache = conf_data['cache'] cache = conf_data['cache']
# Generate the SlicingGroup # 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 # Offset by the bed center
def parse_bed_center(bed_shape_str: str) -> np.ndarray: def parse_bed_center(bed_shape_str: str) -> np.ndarray:
@ -1189,7 +1244,7 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode):
# Prepare 3mf path # Prepare 3mf path
temp_dir = Path(tempfile.gettempdir()) temp_dir = Path(tempfile.gettempdir())
output_path = temp_dir / f"{obj.name}_NodeSlicer.3mf" output_path = temp_dir / f"tmp.3mf"
try: try:
prepare_3mf(output_path, slicing_group, conf_writer, z_gcodes=[]) prepare_3mf(output_path, slicing_group, conf_writer, z_gcodes=[])
@ -1215,6 +1270,7 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode):
# Final check if command exists # Final check if command exists
if not shutil.which(cmd_parts[0]): if not shutil.which(cmd_parts[0]):
if operator: operator.report({'ERROR'}, f"Command or executable not found: {cmd_parts[0]}") if operator: operator.report({'ERROR'}, f"Command or executable not found: {cmd_parts[0]}")
self.gcode_result = ""
return 'error' return 'error'
gcode_path = output_path.with_suffix(".gcode") gcode_path = output_path.with_suffix(".gcode")
@ -1226,6 +1282,7 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode):
process = subprocess.run(cmd, capture_output=True, text=True) process = subprocess.run(cmd, capture_output=True, text=True)
if process.returncode != 0: if process.returncode != 0:
if operator: operator.report({'ERROR'}, f"PrusaSlicer failed: {process.stderr}") if operator: operator.report({'ERROR'}, f"PrusaSlicer failed: {process.stderr}")
self.gcode_result = ""
return 'error' return 'error'
# Read G-code # Read G-code
@ -1233,13 +1290,17 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode):
with open(gcode_path, 'r', encoding='utf-8') as f: with open(gcode_path, 'r', encoding='utf-8') as f:
self.gcode_result = f.read() self.gcode_result = f.read()
output_path.unlink()
if operator: if operator:
operator.report({'INFO'}, f"Slicing complete. G-code generated.") operator.report({'INFO'}, f"Slicing complete. G-code generated.")
else: else:
if operator: operator.report({'ERROR'}, "G-code file was not generated by PrusaSlicer") if operator: operator.report({'ERROR'}, "G-code file was not generated by PrusaSlicer")
self.gcode_result = ""
return 'error' return 'error'
except Exception as e: except Exception as e:
self.gcode_result = ""
if operator: operator.report({'ERROR'}, f"Failed to slice: {str(e)}") if operator: operator.report({'ERROR'}, f"Failed to slice: {str(e)}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
@ -1263,6 +1324,10 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
mesh_name: str = self.resolve("Name") mesh_name: str = self.resolve("Name")
if not gcode: 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' return 'empty'
segments = [] segments = []

View file

@ -163,7 +163,8 @@ class SLICENODE_MT_Menu(Menu):
layout.operator("node.add_node", text="Load Gcode").type = "SLICENODE_LoadGcode" layout.operator("node.add_node", text="Load Gcode").type = "SLICENODE_LoadGcode"
layout.separator() layout.separator()
layout.operator("node.add_node", text="Join Gcode").type = "SLICENODE_JoinGcode" 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 Paths").type = "SLICENODE_JoinPaths"
layout.operator("node.add_node", text="Join Overrides").type = "SLICENODE_JoinOverrides" layout.operator("node.add_node", text="Join Overrides").type = "SLICENODE_JoinOverrides"
layout.operator("node.add_node", text="Join Execution").type = "SLICENODE_JoinExecution" layout.operator("node.add_node", text="Join Execution").type = "SLICENODE_JoinExecution"

View file

@ -1,5 +1,5 @@
from __future__ import annotations from __future__ import annotations
from bpy.types import FloatAttribute, Object, Mesh from bpy.types import Object, Mesh
import bpy import bpy
import zlib, struct import zlib, struct
from functools import cached_property from functools import cached_property
@ -8,7 +8,7 @@ import numpy as np
from numpy import dtype, float64 from numpy import dtype, float64
from numpy.typing import NDArray from numpy.typing import NDArray
from typing import Any, cast from typing import Any
from ..core.geometry import crc32_array from ..core.geometry import crc32_array
from ..infra.blender_bridge import get_all_children from ..infra.blender_bridge import get_all_children
@ -44,7 +44,16 @@ class SlicingObject():
self.mesh = np.empty((0, 4, 3)) self.mesh = np.empty((0, 4, 3))
def offset(self, offset: NDArray): 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 @property
def checksum(self) -> int: def checksum(self) -> int:
@ -99,6 +108,9 @@ class SlicingCollection():
def offset(self, offset: NDArray): def offset(self, offset: NDArray):
for so in self.objects: so.offset(offset) for so in self.objects: so.offset(offset)
def scale(self, factor: float):
for so in self.objects: so.scale(factor)
@property @property
def checksum(self) -> int: def checksum(self) -> int:
return crc32_array(np.array([o.checksum for o in self.objects])) 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) for k, so in self.collections.items(): so.offset(offset)
self.wipe_tower_xy = self.wipe_tower_xy + offset[0:2] 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 @property
def checksum(self): def checksum(self):
buf = bytearray() buf = bytearray()

View file

@ -27,11 +27,11 @@ class SliceNode(Node, AutoBlIdName):
def resolve(self, fieldname) -> Any: def resolve(self, fieldname) -> Any:
return resolve_input(self.inputs.get(fieldname)) 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 from ..registry import get_label_by_name
return self.outputs.new(bl_idname, name or get_label_by_name(bl_idname)) 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 from ..registry import get_label_by_name
sock = self.inputs.new(bl_idname, name or get_label_by_name(bl_idname)) sock = self.inputs.new(bl_idname, name or get_label_by_name(bl_idname))
if multi: if multi: