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 .. 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,7 +1204,11 @@ 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:
@ -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,6 +1282,7 @@ 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
@ -1233,13 +1290,17 @@ class SLICENODE_PrusaslicerSlice(ExecSliceNode):
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 = []

View file

@ -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"

View file

@ -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()

View file

@ -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: