nodeslicer/core/nodeslicer/nodes/mesh.py
2026-08-26 12:54:30 +03:00

959 lines
36 KiB
Python

import copy
from typing import Any
import numpy as np
from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, NfIntProperty, NfFloatProperty, NfStringProperty, NfFloatVectorProperty, notify_tree_changed
from ....registry import register_class
from .base import SliceNode, SlicingCollection, get_all_children, SliceNodeJoiner, SlicingGroup, SlicingObject, SlicePaths
from ....infra.blender.blender_mesh_capture import PdfProperties
from ....core.pdf_exporter.geometry import transform_slice_paths
@register_class
class SLICENODE_GetMesh(SliceNode):
bl_label = "Get Mesh"
include_children: bool = NfBoolProperty(name="Include Children", default=True)
def init(self, context):
self.add_input("NodeSocketObject")
self.add_output("SLICESOCK_Mesh")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "include_children")
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | None:
if socket_name != "Mesh": return None
obj: Any = self.resolve("Object")
if not obj: return None
family = [obj] + (get_all_children(obj) if self.include_children else [])
sc: SlicingCollection = SlicingCollection(family, obj.name)
return sc
@register_class
class SLICENODE_JoinMesh(SliceNodeJoiner):
bl_label = "Join Mesh Parts"
socket_type = "SLICESOCK_Mesh"
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection:
from ....infra.blender.blender_slice_nodes_generic import resolve_input
merged_coll = resolve_input(self.inputs[0]) or SlicingCollection([],'')
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 init(self, context):
self.add_output(self.output_socket_type or self.socket_type, display_shape='LIST')
self.add_multi_socket_group(self.socket_type)
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup:
from ....infra.blender.blender_slice_nodes_generic import resolve_input
group: SlicingGroup = SlicingGroup([])
for input in self.inputs:
coll = resolve_input(input)
if coll:
group.collections[coll.objects[0].name] = coll
return group
@register_class
class SLICENODE_SeparateMesh(SliceNode):
bl_label = "Separate Mesh"
comparison: str = NfEnumProperty(
name="Comparison",
items=[
('EQUAL', "Equal", "=="),
('NOT_EQUAL', "Not Equal", "!="),
('GREATER_THAN', "Greater Than", ">"),
('LESS_THAN', "Less Than", "<"),
('GREATER_EQUAL', "Greater Equal", ">="),
('LESS_EQUAL', "Less Equal", "<="),
],
default='EQUAL',
update=notify_tree_changed
)
def init(self, context):
self.add_input("SLICESOCK_Mesh")
self.add_input("NodeSocketString", "Attribute")
self.add_input("NodeSocketFloat", "Value")
self.add_output("SLICESOCK_Mesh", "Selection")
self.add_output("SLICESOCK_Mesh", "Inverted")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "comparison", text="")
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | None:
coll: SlicingCollection | None = self.resolve("Mesh")
attr_name: str = self.resolve("Attribute")
value: float = self.resolve("Value")
if not coll: return None
res_selection = SlicingCollection([], coll.name + "_selection")
res_inverted = SlicingCollection([], coll.name + "_inverted")
for so in coll.objects:
if attr_name not in so.attributes:
# If attribute missing, entire object goes to inverted
res_inverted.objects.append(so)
continue
attr = so.attributes[attr_name]
vals = attr["values"]
mask = np.zeros(len(so.mesh), dtype=bool)
if self.comparison == 'EQUAL': mask = vals == value
elif self.comparison == 'NOT_EQUAL': mask = vals != value
elif self.comparison == 'GREATER_THAN': mask = vals > value
elif self.comparison == 'LESS_THAN': mask = vals < value
elif self.comparison == 'GREATER_EQUAL': mask = vals >= value
elif self.comparison == 'LESS_EQUAL': mask = vals <= value
if np.all(mask):
res_selection.objects.append(so)
elif not np.any(mask):
res_inverted.objects.append(so)
else:
# Split the object
so_sel = SlicingObject()
so_sel.name = so.name + "_sel"
so_sel.parent = so.parent
so_sel.object_type = so.object_type
so_sel.extruder = so.extruder
so_sel.mesh = so.mesh[mask]
for k, v in so.attributes.items():
so_sel.attributes[k] = {
"domain": v["domain"],
"type": v["type"],
"values": v["values"][mask]
}
res_selection.objects.append(so_sel)
so_inv = SlicingObject()
so_inv.name = so.name + "_inv"
so_inv.parent = so.parent
so_inv.object_type = so.object_type
so_inv.extruder = so.extruder
so_inv.mesh = so.mesh[~mask]
for k, v in so.attributes.items():
so_inv.attributes[k] = {
"domain": v["domain"],
"type": v["type"],
"values": v["values"][~mask]
}
res_inverted.objects.append(so_inv)
if socket_name == "Selection":
return res_selection
return res_inverted
@register_class
class SLICENODE_SeparateMeshByRange(SliceNode):
bl_label = "Separate Mesh by Range"
start_val: int = NfIntProperty(name="Start", default=0, update=notify_tree_changed)
end_val: int = NfIntProperty(name="End", default=1, update=notify_tree_changed)
def init(self, context):
self.add_input("SLICESOCK_Mesh")
self.add_input("NodeSocketString", "Attribute")
self._update_outputs()
def update(self):
super().update()
self._update_outputs()
def _update_outputs(self):
# Ensure end >= start
start = int(self.start_val) if isinstance(getattr(self, "start_val", None), (int, float)) else 0
end_val = int(self.end_val) if isinstance(getattr(self, "end_val", None), (int, float)) else 1
end = max(start, end_val)
expected_ids = [str(i) for i in range(start, end + 1)]
# Remove outputs that are no longer in the range
# We use a list to avoid modification-during-iteration issues
to_remove = [o for o in self.outputs if o.identifier not in expected_ids]
for o in to_remove:
self.outputs.remove(o)
# Add new outputs
for id_str in expected_ids:
if id_str not in self.outputs:
self.add_output("SLICESOCK_Mesh", id_str, identifier=id_str)
# Re-order to match range
for i, id_str in enumerate(expected_ids):
current_index = self.outputs.find(id_str)
if current_index != -1 and current_index != i:
self.outputs.move(current_index, i)
def draw_buttons(self, context, layout):
assert layout
col = layout.column(align=True)
col.prop(self, "start_val")
col.prop(self, "end_val")
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | None:
coll: SlicingCollection | None = self.resolve("Mesh")
attr_name: str = self.resolve("Attribute")
if not coll: return None
# Check if we can parse the socket name as an integer
try:
target_val = int(socket_name)
except ValueError:
return None
res = SlicingCollection([], f"{coll.name}_{target_val}")
start = self.start_val
end = max(start, self.end_val)
total_parts = end - start + 1
num_digits = max(2, len(str(total_parts)))
i = target_val - start
for so in coll.objects:
if attr_name not in so.attributes:
continue
attr = so.attributes[attr_name]
vals = attr["values"]
# Attribute values might be floats or ints, we check for equality
mask = vals == target_val
if np.any(mask):
# Split the object
so_new = SlicingObject()
base_mesh_name = so.name
so_new.name = f"{base_mesh_name}{i:0{num_digits}d}"
so_new.parent = so.parent
so_new.object_type = so.object_type
so_new.extruder = so.extruder
so_new.mesh = so.mesh[mask]
so_new.transforms = [dict(t) for t in getattr(so, "transforms", [])]
so_new.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
so_new.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
so_new.modifiers = so.modifiers.copy() if so.modifiers else []
for k, v in so.attributes.items():
so_new.attributes[k] = {
"domain": v["domain"],
"type": v["type"],
"values": v["values"][mask]
}
res.objects.append(so_new)
return res
@register_class
class SLICENODE_ArrangeMesh(SliceNode):
bl_label = "Arrange Slicing Group"
method: str = NfEnumProperty(
name="Method",
items=[
('NFP', "NFP (Default)", "Nest using No-Fit Polygon placer + First-Fit selection"),
('BL', "Bottom-Left", "Nest using Bottom-Left placer + First-Fit selection"),
('DJD', "DJD Heuristic", "Nest using No-Fit Polygon placer + DJD Heuristic selection"),
],
default='NFP',
update=notify_tree_changed
)
allow_rotations: bool = NfBoolProperty(
name="Allow Rotations",
default=True,
update=notify_tree_changed
)
def init(self, context):
self.add_input("SLICESOCK_Mesh", "Slicing Group", display_shape='LIST')
self.add_input("NodeSocketFloat", "Bed X").value = 250.0
self.add_input("NodeSocketFloat", "Bed Y").value = 250.0
self.add_input("NodeSocketFloat", "Spacing").value = 2.0
self.add_output("SLICESOCK_Mesh", "Slicing Group", display_shape='LIST')
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "method", text="")
layout.prop(self, "allow_rotations")
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup | None:
if socket_name != "Slicing Group":
return None
mesh_data: SlicingCollection | SlicingGroup | None = self.resolve("Slicing Group")
if not mesh_data:
return None
bed_x: float = self.resolve("Bed X") or 250.0
bed_y: float = self.resolve("Bed Y") or 250.0
spacing: float = self.resolve("Spacing") or 2.0
# Convert input to SlicingGroup if it's a SlicingCollection
if isinstance(mesh_data, SlicingGroup):
slicing_group = mesh_data
else:
slicing_group = SlicingGroup([])
slicing_group.collections = {mesh_data.name: mesh_data}
# Create a deep copy of the group to avoid mutating cached inputs
copied_group = _copy_slicing_group(slicing_group)
# Prepare the list of items to pack
import pynest2d
items_list = []
collection_map = {}
for name, sc in copied_group.collections.items():
if sc.min_x is None or sc.max_x is None or sc.min_y is None or sc.max_y is None:
continue
w_mm = sc.max_x - sc.min_x
h_mm = sc.max_y - sc.min_y
if w_mm <= 0 or h_mm <= 0:
continue
w = pynest2d.mm(w_mm)
h = pynest2d.mm(h_mm)
item = pynest2d.Rectangle(w, h)
items_list.append(item)
collection_map[id(item)] = (name, sc)
if not items_list:
return copied_group
# Define the bin
bin_w = pynest2d.mm(bed_x)
bin_h = pynest2d.mm(bed_y)
bin_center = pynest2d.Point(pynest2d.mm(bed_x / 2.0), pynest2d.mm(bed_y / 2.0))
box = pynest2d.Box(bin_w, bin_h, bin_center)
# Spacing in internal units
spacing_units = pynest2d.mm(spacing)
# Configure and run the selected placer
placer = getattr(self, "method", "NFP")
allow_rot = getattr(self, "allow_rotations", True)
if placer == 'BL':
config = pynest2d.BLConfig()
config.allow_rotations = allow_rot
pynest2d.nest_blp(items_list, box, spacing_units, config)
elif placer == 'DJD':
placer_config = pynest2d.NfpConfig()
if not allow_rot:
placer_config.rotations = [0.0]
pynest2d.nest_djd(items_list, box, spacing_units, placer_config)
else: # 'NFP'
config = pynest2d.NfpConfig()
if not allow_rot:
config.rotations = [0.0]
pynest2d.nest(items_list, box, spacing_units, config)
# Apply the resulting translations and rotations to the copied collections
for item in items_list:
bin_id = item.binId()
if bin_id == 0:
name, sc = collection_map[id(item)]
trans = item.translation()
rot = item.rotation()
tx = trans.x() / pynest2d.MM_IN_COORDS
ty = trans.y() / pynest2d.MM_IN_COORDS
min_x = sc.min_x
min_y = sc.min_y
cos_val = np.cos(rot)
sin_val = np.sin(rot)
for so in sc.objects:
if so.mesh is None or so.mesh.size == 0:
continue
vx = so.mesh[:, :3, 0]
vy = so.mesh[:, :3, 1]
vx_new = (vx - min_x) * cos_val - (vy - min_y) * sin_val + tx
vy_new = (vx - min_x) * sin_val + (vy - min_y) * cos_val + ty
so.mesh[:, :3, 0] = vx_new
so.mesh[:, :3, 1] = vy_new
nx = so.mesh[:, 3, 0]
ny = so.mesh[:, 3, 1]
nx_new = nx * cos_val - ny * sin_val
ny_new = nx * sin_val + ny * cos_val
so.mesh[:, 3, 0] = nx_new
so.mesh[:, 3, 1] = ny_new
else:
name, sc = collection_map[id(item)]
print(f"Warning: Collection '{name}' did not fit on the build plate (binId: {bin_id})")
return copied_group
def _copy_slicing_object(so: SlicingObject) -> SlicingObject:
new_so = SlicingObject()
new_so.name = so.name
new_so.parent = so.parent
new_so.mesh = so.mesh.copy() if so.mesh is not None else np.empty((0, 4, 3))
new_so.attributes = {}
for k, v in so.attributes.items():
if isinstance(v, dict):
new_so.attributes[k] = {
"domain": v.get("domain", "face"),
"type": v.get("type", "string"),
"values": v["values"].copy() if isinstance(v.get("values"), np.ndarray) else v.get("values")
}
elif isinstance(v, np.ndarray):
new_so.attributes[k] = v.copy()
elif isinstance(v, list):
new_so.attributes[k] = list(v)
else:
new_so.attributes[k] = copy.deepcopy(v)
new_so.object_type = so.object_type
new_so.modifiers = so.modifiers.copy() if so.modifiers else []
new_so.extruder = so.extruder
new_so.transforms = [dict(t) for t in getattr(so, "transforms", [])]
new_so.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
new_so.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
return new_so
def _copy_slicing_collection(sc: SlicingCollection) -> SlicingCollection:
new_sc = SlicingCollection([], sc.name)
new_sc.objects = [_copy_slicing_object(so) for so in sc.objects]
new_sc.transforms = [dict(t) for t in getattr(sc, "transforms", [])]
return new_sc
def _copy_slicing_group(sg: SlicingGroup) -> SlicingGroup:
new_sg = SlicingGroup([])
new_sg.collections = {k: _copy_slicing_collection(sc) for k, sc in sg.collections.items()}
new_sg.wipe_tower_xy = sg.wipe_tower_xy.copy()
new_sg.wipe_tower_rotation_deg = sg.wipe_tower_rotation_deg
new_sg.object_offset = sg.object_offset.copy()
new_sg.transforms = [dict(t) for t in getattr(sg, "transforms", [])]
return new_sg
def _transform_geometry_item(geom: Any, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray, t_rec: dict[str, Any]) -> Any:
if geom is None:
return None
elif isinstance(geom, SlicePaths):
copied_data = transform_slice_paths(geom, t_arr, r_arr, s_arr)
if not hasattr(copied_data, "transforms") or copied_data.transforms is None:
copied_data.transforms = []
else:
copied_data.transforms = [dict(t) for t in copied_data.transforms]
copied_data.transforms.append(t_rec)
return copied_data
elif isinstance(geom, (list, tuple)):
return [_transform_geometry_item(item, t_arr, r_arr, s_arr, t_rec) for item in geom]
elif isinstance(geom, SlicingGroup):
copied_data = _copy_slicing_group(geom)
for col in copied_data.collections.values():
if any(s != 1.0 for s in s_arr):
col.scale(s_arr)
if any(r != 0.0 for r in r_arr):
col.rotate(r_arr)
if any(t != 0.0 for t in t_arr):
col.offset(t_arr)
if any(t != 0.0 for t in t_arr):
copied_data.wipe_tower_xy = copied_data.wipe_tower_xy + t_arr[0:2]
if any(r != 0.0 for r in r_arr):
rz = r_arr[2]
cz, sz = np.cos(rz), np.sin(rz)
rot_mat_2d = np.array([[cz, -sz], [sz, cz]])
copied_data.wipe_tower_xy = copied_data.wipe_tower_xy @ rot_mat_2d.T
import math
copied_data.wipe_tower_rotation_deg = round(copied_data.wipe_tower_rotation_deg + rz * 180 / math.pi, 5)
if any(s != 1.0 for s in s_arr):
copied_data.wipe_tower_xy *= s_arr[0:2]
if not hasattr(copied_data, "transforms") or copied_data.transforms is None:
copied_data.transforms = []
copied_data.transforms.append(t_rec)
return copied_data
elif isinstance(geom, SlicingCollection):
copied_data = _copy_slicing_collection(geom)
if any(s != 1.0 for s in s_arr):
copied_data.scale(s_arr)
if any(r != 0.0 for r in r_arr):
copied_data.rotate(r_arr)
if any(t != 0.0 for t in t_arr):
copied_data.offset(t_arr)
if not hasattr(copied_data, "transforms") or copied_data.transforms is None:
copied_data.transforms = []
copied_data.transforms.append(t_rec)
return copied_data
elif isinstance(geom, SlicingObject):
copied_data = _copy_slicing_object(geom)
if any(s != 1.0 for s in s_arr):
copied_data.scale(s_arr)
if any(r != 0.0 for r in r_arr):
copied_data.rotate(r_arr)
if any(t != 0.0 for t in t_arr):
copied_data.offset(t_arr)
if not hasattr(copied_data, "transforms") or copied_data.transforms is None:
copied_data.transforms = []
copied_data.transforms.append(t_rec)
return copied_data
return geom
@register_class
class SLICENODE_TransformGeometry(SliceNode):
bl_label = "Transform Geometry"
def init(self, context):
self.add_input("SLICESOCK_Geometry", "Geometry")
self.add_input("NodeSocketVector", "Translation")
self.add_input("NodeSocketRotation", "Rotation")
self.add_input("NodeSocketVector", "Scale").value = (1.0, 1.0, 1.0)
self.add_output("SLICESOCK_Geometry", "Geometry")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name not in ("Geometry",):
return None
geom_data: Any = self.resolve("Geometry")
if geom_data is None:
return None
translation = self.resolve("Translation") or (0.0, 0.0, 0.0)
rotation = self.resolve("Rotation") or (0.0, 0.0, 0.0)
scale = self.resolve("Scale") or (1.0, 1.0, 1.0)
t_arr = np.array(translation, dtype=np.float64)
r_arr = np.array(rotation, dtype=np.float64)
s_arr = np.array(scale, dtype=np.float64)
t_rec = {
"type": "transform_geometry",
"translation": (float(t_arr[0]), float(t_arr[1]), float(t_arr[2])),
"rotation": (float(r_arr[0]), float(r_arr[1]), float(r_arr[2])),
"scale": (float(s_arr[0]), float(s_arr[1]), float(s_arr[2])),
}
return _transform_geometry_item(geom_data, t_arr, r_arr, s_arr, t_rec)
@register_class
class SLICENODE_DropGeometry(SliceNode):
bl_label = "Drop Geometry"
def init(self, context):
self.add_input("SLICESOCK_Mesh", "Mesh")
self.add_input("NodeSocketFloat", "Z")
self.add_output("SLICESOCK_Mesh", "Mesh")
self.add_output("NodeSocketVector", "Translation")
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | SlicingGroup | tuple[float, float, float] | None:
if socket_name == "Translation":
mesh_data: SlicingCollection | SlicingGroup | None = self.resolve("Mesh")
if not mesh_data:
return None
z = self.resolve('Z')
if z is None:
z = 0.0
assert mesh_data.min_z is not None
min_z = round(mesh_data.min_z, 6)
offset_z = float(z - min_z)
return (0.0, 0.0, offset_z)
if socket_name != "Mesh":
return None
mesh_data = self.resolve("Mesh")
if not mesh_data:
return None
z = self.resolve('Z')
if z is None:
z = 0.0
assert mesh_data.min_z is not None
min_z = round(mesh_data.min_z, 6)
offset_z = float(z - min_z)
if isinstance(mesh_data, SlicingGroup):
copied_data = _copy_slicing_group(mesh_data)
else:
copied_data = _copy_slicing_collection(mesh_data)
copied_data.offset(np.array([0.0, 0.0, offset_z], dtype=np.float64))
drop_rec = {
"type": "drop_geometry",
"target_z": float(z),
"offset_z": float(offset_z),
"translation": (0.0, 0.0, float(offset_z)),
"rotation": (0.0, 0.0, 0.0),
"scale": (1.0, 1.0, 1.0),
}
if not hasattr(copied_data, "transforms"):
copied_data.transforms = []
copied_data.transforms.append(drop_rec)
return copied_data
def format_name_template(template: str, so: Any, default_old_name: str = "") -> str:
if not template or "{" not in template or "}" not in template:
return template
import re
from ....core.pdf_exporter.metadata import get_first_attribute_value
def replace_match(match: re.Match[str]) -> str:
attr_name = match.group(1).strip()
val = get_first_attribute_value(so, attr_name)
if val is None and attr_name in ("Name", "name"):
val = getattr(so, "name", "") or default_old_name
if val is None:
return ""
if isinstance(val, float):
return f"{val:g}"
return str(val)
return re.sub(r"\{([^}]+)\}", replace_match, template)
def _rename_slicing_item(geom: Any, new_name: str) -> Any:
if geom is None or not new_name:
return geom
elif isinstance(geom, SlicePaths):
copied = geom.copy_with(paths=list(geom))
target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", ""))
copied.name = target_name
return copied
elif isinstance(geom, (list, tuple)):
return [_rename_slicing_item(item, f"{new_name}_{i}" if (len(geom) > 1 and "{" not in new_name) else new_name) for i, item in enumerate(geom)]
elif isinstance(geom, SlicingGroup):
copied = _copy_slicing_group(geom)
old_colls = dict(copied.collections)
new_colls = {}
for i, (old_key, coll) in enumerate(old_colls.items()):
coll_old_name = getattr(coll, "name", old_key)
target_name = format_name_template(new_name, coll, default_old_name=coll_old_name)
coll_new_name = f"{target_name}_{i}" if (len(old_colls) > 1 and "{" not in new_name and target_name == new_name) else target_name
new_colls[coll_new_name] = _rename_slicing_item(coll, coll_new_name)
copied.collections = new_colls
return copied
elif isinstance(geom, SlicingCollection):
copied = _copy_slicing_collection(geom)
target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", ""))
copied.name = target_name
for i, so in enumerate(copied.objects):
so.parent = target_name
so_old_name = getattr(so, "name", "")
so_target = format_name_template(new_name, so, default_old_name=so_old_name)
so.name = f"{so_target}_{i}" if (len(copied.objects) > 1 and "{" not in new_name and so_target == new_name) else so_target
return copied
elif isinstance(geom, SlicingObject):
copied = _copy_slicing_object(geom)
target_name = format_name_template(new_name, copied, default_old_name=getattr(copied, "name", ""))
copied.name = target_name
return copied
return geom
@register_class
class SLICENODE_RenameMesh(SliceNode):
bl_label = "Rename Mesh"
def init(self, context):
self.add_input("SLICESOCK_Mesh", "Geometry")
self.add_input("NodeSocketString", "Name")
self.add_output("SLICESOCK_Mesh", "Geometry")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Geometry":
return None
geom: Any = self.resolve("Geometry")
if geom is None:
return None
name_socket: str | None = self.resolve("Name")
target_name = name_socket
if not target_name:
return geom
return _rename_slicing_item(geom, target_name)
def _assign_pdf_properties_to_item(geom: Any, line_width: float, line_color: tuple[float, float, float] | None, show_name: bool) -> Any:
if geom is None:
return None
elif isinstance(geom, SlicePaths):
copied = geom.copy_with(paths=list(geom))
copied.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name)
return copied
elif isinstance(geom, (list, tuple)):
return [_assign_pdf_properties_to_item(item, line_width, line_color, show_name) for item in geom]
elif isinstance(geom, SlicingGroup):
copied = _copy_slicing_group(geom)
new_colls = {}
for k, coll in copied.collections.items():
new_colls[k] = _assign_pdf_properties_to_item(coll, line_width, line_color, show_name)
copied.collections = new_colls
return copied
elif isinstance(geom, SlicingCollection):
copied = _copy_slicing_collection(geom)
copied.objects = [_assign_pdf_properties_to_item(so, line_width, line_color, show_name) for so in copied.objects]
return copied
elif isinstance(geom, SlicingObject):
copied = _copy_slicing_object(geom)
copied.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name)
return copied
return geom
@register_class
class SLICENODE_AssignPdfProperties(SliceNode):
bl_label = "Assign PDF Properties"
line_width: float = NfFloatProperty(name="Line Width (mm)", default=0.0, min=0.0, max=50.0)
line_color: tuple[float, float, float] = NfFloatVectorProperty(
name="Line Color",
subtype='COLOR',
default=(0.0, 0.0, 0.0),
min=0.0,
max=1.0,
size=3
)
show_name: bool = NfBoolProperty(name="Show Name", default=False)
def init(self, context):
self.add_input("SLICESOCK_Mesh", "Geometry")
self.add_input("NodeSocketFloat", "Line Width")
self.add_output("SLICESOCK_Mesh", "Geometry")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "line_color")
layout.prop(self, "show_name")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Geometry":
return None
geom: Any = self.resolve("Geometry")
if not geom:
return None
width_socket = self.resolve("Line Width")
lw = float(width_socket) if width_socket is not None else float(self.line_width)
lc = (float(self.line_color[0]), float(self.line_color[1]), float(self.line_color[2]))
sn = bool(self.show_name)
return _assign_pdf_properties_to_item(geom, lw, lc, sn)
def _assign_properties_to_item(geom: Any, props: list[tuple[str, str]]) -> Any:
from ....core.pdf_exporter.metadata import resolve_object_properties
if geom is None:
return None
elif isinstance(geom, SlicePaths):
copied = geom.copy_with(paths=list(geom))
copied.pdf_object_properties = resolve_object_properties(copied, props)
return copied
elif isinstance(geom, (list, tuple)):
return [_assign_properties_to_item(item, props) for item in geom]
elif isinstance(geom, SlicingGroup):
copied = _copy_slicing_group(geom)
for coll in copied.collections.values():
for obj in coll.objects:
obj.pdf_object_properties = resolve_object_properties(obj, props)
return copied
elif isinstance(geom, SlicingCollection):
copied = _copy_slicing_collection(geom)
for obj in copied.objects:
obj.pdf_object_properties = resolve_object_properties(obj, props)
return copied
elif isinstance(geom, SlicingObject):
copied = _copy_slicing_object(geom)
copied.pdf_object_properties = resolve_object_properties(copied, props)
return copied
return geom
@register_class
class SLICENODE_AssignPropertyToObject(SliceNode):
bl_label = "Assign PDF Object Properties"
def init(self, context):
self.add_input("SLICESOCK_Mesh", "Geometry")
self.add_input("SLICESOCK_Settings", "Settings")
self.add_output("SLICESOCK_Mesh", "Geometry")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Geometry":
return None
geom: Any = self.resolve("Geometry")
if not geom:
return None
raw_settings = self.resolve("Settings")
props: list[tuple[str, str]] = []
if raw_settings:
if isinstance(raw_settings, dict):
for k, v in raw_settings.items():
props.append((str(k), str(v)))
elif isinstance(raw_settings, (list, tuple)):
for item in raw_settings:
if isinstance(item, tuple) and len(item) == 2:
props.append((str(item[0]), str(item[1])))
elif isinstance(item, dict):
for k, v in item.items():
props.append((str(k), str(v)))
else:
props.append(("", str(item)))
return _assign_properties_to_item(geom, props)
OBJECT_TYPE_ITEMS: list[tuple[str, str, str]] = [
("NO_CHANGE", "Keep Original", "Do not modify the object/part type"),
("ModelPart", "Model Part", "Standard model part"),
("ParameterModifier", "Modifier", "Parameter modifier volume"),
("NegativeVolume", "Negative Volume", "Negative volume for subtractions"),
("SupportBlocker", "Support Blocker", "Support blocker volume"),
("SupportEnforcer", "Support Enforcer", "Support enforcer volume"),
("WipeTower", "Wipe Tower", "Wipe tower marker"),
]
EXTRUDER_ITEMS: list[tuple[str, str, str]] = [
("NO_CHANGE", "Keep Original", "Do not modify extruder assignment"),
("0", "Default Extruder", "Default Extruder"),
("1", "Extruder 1", "Extruder 1"),
("2", "Extruder 2", "Extruder 2"),
("3", "Extruder 3", "Extruder 3"),
("4", "Extruder 4", "Extruder 4"),
("5", "Extruder 5", "Extruder 5"),
]
def _apply_modifiers_to_so(
so: SlicingObject,
part_type: str,
extruder: str,
modifiers_dict: dict[str, str] | None
) -> None:
if part_type != "NO_CHANGE":
so.object_type = part_type
if extruder != "NO_CHANGE":
so.extruder = int(extruder)
if modifiers_dict:
existing = {m['param_id']: str(m['param_value']) for m in getattr(so, 'modifiers', []) if m and 'param_id' in m}
for k, v in modifiers_dict.items():
existing[str(k)] = str(v)
so.modifiers = [{'param_id': k, 'param_value': v} for k, v in existing.items()]
def _assign_modifiers_to_item(
geom: Any,
part_type: str = "NO_CHANGE",
extruder: str = "NO_CHANGE",
modifiers_dict: dict[str, str] | None = None
) -> Any:
if geom is None:
return None
elif isinstance(geom, (list, tuple)):
return [_assign_modifiers_to_item(item, part_type, extruder, modifiers_dict) for item in geom]
elif isinstance(geom, SlicingGroup):
copied = _copy_slicing_group(geom)
for coll in copied.collections.values():
for obj in coll.objects:
_apply_modifiers_to_so(obj, part_type, extruder, modifiers_dict)
return copied
elif isinstance(geom, SlicingCollection):
copied = _copy_slicing_collection(geom)
for obj in copied.objects:
_apply_modifiers_to_so(obj, part_type, extruder, modifiers_dict)
return copied
elif isinstance(geom, SlicingObject):
copied = _copy_slicing_object(geom)
_apply_modifiers_to_so(copied, part_type, extruder, modifiers_dict)
return copied
return geom
@register_class
class SLICENODE_AssignModifiers(SliceNode):
bl_label = "Assign Modifiers"
part_type: str = NfEnumProperty(
name="Part Type",
description="PrusaSlicer part/volume type",
items=OBJECT_TYPE_ITEMS,
default="NO_CHANGE"
)
extruder: str = NfEnumProperty(
name="Extruder",
description="Extruder assignment",
items=EXTRUDER_ITEMS,
default="NO_CHANGE"
)
def init(self, context):
self.add_input("SLICESOCK_Mesh", "Geometry")
self.add_input("SLICESOCK_Settings", "Modifiers")
self.add_output("SLICESOCK_Mesh", "Geometry")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "part_type", text="Type")
layout.prop(self, "extruder", text="Extruder")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Geometry":
return None
geom: Any = self.resolve("Geometry")
if not geom:
return None
raw_modifiers = self.resolve("Modifiers")
if raw_modifiers is None:
raw_modifiers = self.resolve("Settings")
mod_dict: dict[str, str] = {}
if raw_modifiers:
if isinstance(raw_modifiers, dict):
for k, v in raw_modifiers.items():
mod_dict[str(k)] = str(v)
elif isinstance(raw_modifiers, (list, tuple)):
for item in raw_modifiers:
if isinstance(item, tuple) and len(item) == 2:
mod_dict[str(item[0])] = str(item[1])
elif isinstance(item, dict):
for k, v in item.items():
mod_dict[str(k)] = str(v)
return _assign_modifiers_to_item(geom, self.part_type, self.extruder, mod_dict if mod_dict else None)