nodeslicer/core/nodeslicer/nodes/mesh.py

891 lines
38 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, SliceNodeListJoiner, SlicingGroup, SlicingObject, SlicePaths
from ....infra.blender.blender_mesh_capture import PdfProperties
from ....core.pdf_exporter.geometry import transform_slice_paths
from ....core.geometry import update_transform
from ....infra.blender.list_utils import eval_element_wise, eval_element_wise_multivalue, prepare_indexed_inputs, flatten_list, is_list_data, is_input_a_list
@register_class
class SLICENODE_GetMesh(SliceNode):
bl_label = "Get Mesh"
list_socket_mapping = [("Object", "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) -> Any:
if socket_name != "Mesh": return None
obj: Any = self.resolve("Object")
if not obj: return None
from ....infra.blender.blender_slice_nodes_generic import object_to_mesh
return object_to_mesh(obj, include_children=self.include_children)
@register_class
class SLICENODE_MeshesToList(SliceNodeListJoiner):
bl_label = "Meshes to List"
socket_type = "SLICESOCK_Mesh"
socket_label = "Mesh"
output_socket_name = "Meshes"
@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
all_colls = []
for inp in self.inputs:
if getattr(inp, "is_linked", False) or getattr(inp, "value", None):
val = resolve_input(inp)
if val:
all_colls.extend(flatten_list(val))
first_coll = all_colls[0] if all_colls else None
merged_coll = SlicingCollection([], first_coll.name if first_coll else '')
for coll in all_colls:
if hasattr(coll, "objects"):
merged_coll.objects += list(coll.objects)
elif isinstance(coll, SlicingObject):
merged_coll.objects.append(coll)
for t in getattr(coll, "transforms", []):
if t not in merged_coll.transforms:
merged_coll.transforms.append(dict(t))
return merged_coll
@register_class
class SLICENODE_JoinMeshGroups(SliceNodeJoiner):
bl_label = "Join Mesh Groups"
socket_type = "SLICESOCK_Mesh"
is_list_output = True
always_list_outputs = ["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([])
all_colls = []
for input_socket in self.inputs:
if getattr(input_socket, "is_linked", False) or getattr(input_socket, "value", None):
val = resolve_input(input_socket)
if val:
all_colls.extend(flatten_list(val))
for idx, coll in enumerate(all_colls):
col_name = getattr(coll, "name", "")
if not col_name and getattr(coll, "objects", None):
col_name = coll.objects[0].name
if not col_name:
col_name = f"collection_{idx}"
group.collections[col_name] = coll
return group
@register_class
class SLICENODE_SeparateMesh(SliceNode):
bl_label = "Separate Mesh"
list_socket_mapping = [("Mesh", ["Selection", "Inverted"])]
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) -> Any:
def _split_coll(sc: SlicingCollection, a_name: str, comp_val: float) -> tuple[SlicingCollection, SlicingCollection]:
res_selection = SlicingCollection([], sc.name + "_selection")
res_inverted = SlicingCollection([], sc.name + "_inverted")
for so in sc.objects:
if not a_name or a_name not in so.attributes:
res_inverted.objects.append(so)
continue
attr = so.attributes[a_name]
vals = attr["values"]
mask = np.zeros(len(so.mesh), dtype=bool)
if self.comparison == 'EQUAL': mask = vals == comp_val
elif self.comparison == 'NOT_EQUAL': mask = vals != comp_val
elif self.comparison == 'GREATER_THAN': mask = vals > comp_val
elif self.comparison == 'LESS_THAN': mask = vals < comp_val
elif self.comparison == 'GREATER_EQUAL': mask = vals >= comp_val
elif self.comparison == 'LESS_EQUAL': mask = vals <= comp_val
if np.all(mask):
res_selection.objects.append(so)
elif not np.any(mask):
res_inverted.objects.append(so)
else:
so_sel = SlicingObject()
so_sel.name = so.name + "_sel"
so_sel.parent = so.parent
so_sel.mesh = so.mesh[mask]
so_sel.transforms = [dict(t) for t in getattr(so, "transforms", [])]
so_sel.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
so_sel.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
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.mesh = so.mesh[~mask]
so_inv.transforms = [dict(t) for t in getattr(so, "transforms", [])]
so_inv.pdf_properties = copy.deepcopy(so.pdf_properties) if hasattr(so, "pdf_properties") and so.pdf_properties else PdfProperties()
so_inv.pdf_object_properties = list(getattr(so, "pdf_object_properties", []))
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)
return res_selection, res_inverted
return self.eval_multivalue(
lambda sc, a, v: _split_coll(sc, str(a or ""), float(v if v is not None else 0.0)),
main_socket="Mesh",
secondary_sockets=["Attribute", "Value"],
socket_name=socket_name,
)
@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)]
from ....infra.blender.list_utils import is_socket_linked_to_list, set_socket_shape
inp = self.inputs.get("Mesh") if hasattr(self, "inputs") else None
is_list = is_socket_linked_to_list(inp) if inp is not None else False
if inp is not None:
set_socket_shape(inp, "LIST" if is_list else "CIRCLE")
attr_inp = self.inputs.get("Attribute") if hasattr(self, "inputs") else None
if attr_inp is not None:
set_socket_shape(attr_inp, "LIST" if is_socket_linked_to_list(attr_inp) else "CIRCLE")
# Remove outputs that are no longer in the range
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, display_shape="LIST" if is_list else "CIRCLE")
else:
set_socket_shape(self.outputs[id_str], "LIST" if is_list else "CIRCLE")
# 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) -> Any:
try:
target_val = int(socket_name)
except ValueError:
return None
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)
total_parts = end - start + 1
num_digits = max(2, len(str(total_parts)))
i = target_val - start
def _split_coll(sc: SlicingCollection, a_name: str) -> SlicingCollection:
res = SlicingCollection([], f"{sc.name}_{target_val}")
for so in sc.objects:
if not a_name or a_name not in so.attributes:
continue
attr = so.attributes[a_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.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", []))
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
return self.eval_multivalue(_split_coll, "Mesh", ["Attribute"])
@register_class
class SLICENODE_ArrangeMesh(SliceNode):
bl_label = "Arrange Slicing Group"
list_socket_mapping = [("Slicing Group", "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")
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")
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 or list
is_list = is_list_data(mesh_data)
if isinstance(mesh_data, SlicingGroup):
slicing_group = mesh_data
elif is_list:
slicing_group = SlicingGroup([])
for idx, item in enumerate(mesh_data):
if isinstance(item, SlicingGroup):
for k, sc in item.collections.items():
slicing_group.collections[f"{k}_{idx}"] = sc
elif isinstance(item, SlicingCollection):
col_name = item.name or f"mesh_{idx}"
slicing_group.collections[col_name] = item
elif isinstance(item, SlicingObject):
sc = SlicingCollection([item], item.name or f"mesh_{idx}")
slicing_group.collections[sc.name] = sc
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)]
if is_list:
return list(copied_group.collections.values())
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.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.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) -> Any:
if geom is None:
return None
elif isinstance(geom, SlicePaths):
copied_data = transform_slice_paths(geom, t_arr, r_arr, s_arr)
copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr)
return copied_data
elif isinstance(geom, (list, tuple)):
return [_transform_geometry_item(item, t_arr, r_arr, s_arr) 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)
col.transforms = update_transform(getattr(col, "transforms", []), t_arr, r_arr, s_arr)
for so in col.objects:
so.transforms = update_transform(getattr(so, "transforms", []), t_arr, r_arr, s_arr)
copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr)
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)
for so in copied_data.objects:
so.transforms = update_transform(getattr(so, "transforms", []), t_arr, r_arr, s_arr)
copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr)
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)
copied_data.transforms = update_transform(getattr(geom, "transforms", []), t_arr, r_arr, s_arr)
return copied_data
elif hasattr(geom, "location") and hasattr(geom, "rotation_euler") and hasattr(geom, "scale") and not hasattr(geom, "mesh"):
from ....infra.blender.blender_slice_nodes_generic import object_to_mesh
mesh_geom = object_to_mesh(geom, include_children=True)
return _transform_geometry_item(mesh_geom, t_arr, r_arr, s_arr)
return geom
@register_class
class SLICENODE_TransformGeometry(SliceNode):
bl_label = "Transform Geometry"
list_socket_mapping = [("Geometry", "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:
def _transform_one(g: Any, t: Any, r: Any, s: Any) -> Any:
t_arr = np.array(t if t is not None else (0.0, 0.0, 0.0), dtype=np.float64)
r_arr = np.array(r if r is not None else (0.0, 0.0, 0.0), dtype=np.float64)
s_arr = np.array(s if s is not None else (1.0, 1.0, 1.0), dtype=np.float64)
return _transform_geometry_item(g, t_arr, r_arr, s_arr)
return self.eval_multivalue(
_transform_one,
main_socket="Geometry",
secondary_sockets=["Translation", "Rotation", "Scale"],
is_vector=[True, True, True],
socket_name=socket_name,
)
@register_class
class SLICENODE_GetTransforms(SliceNode):
bl_label = "Get Transforms"
list_socket_mapping = [("Geometry", ["Translation", "Rotation", "Scale"])]
def init(self, context):
self.add_input("SLICESOCK_Geometry", "Geometry")
self.add_output("NodeSocketVector", "Translation")
self.add_output("NodeSocketRotation", "Rotation")
self.add_output("NodeSocketVector", "Scale")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
from ....core.slice import extract_geometry_transforms
from ....infra.blender.list_utils import is_input_a_list
sock_lower = socket_name.lower()
default = (1.0, 1.0, 1.0) if sock_lower == "scale" else (0.0, 0.0, 0.0)
idx = 0 if sock_lower == "translation" else (1 if sock_lower == "rotation" else 2)
geom = self.resolve("Geometry")
if geom is None:
return default
is_list = is_input_a_list(geom, is_vector=False)
if not is_list and isinstance(geom, (list, tuple)):
if type(geom).__name__ != "SlicePaths" and not getattr(geom, "is_slice_paths", False):
is_list = True
elif not is_list and hasattr(geom, "__iter__") and not hasattr(geom, "shape") and not isinstance(geom, (str, bytes, dict, set)) and type(geom).__name__ != "SlicePaths" and not getattr(geom, "is_slice_paths", False) and not hasattr(geom, "mesh") and not hasattr(geom, "objects") and not hasattr(geom, "collections") and not hasattr(geom, "location"):
is_list = True
if is_list:
items = list(geom)
if len(items) == 0:
return []
results = []
for item in items:
trans = extract_geometry_transforms(item)
if isinstance(trans, tuple) and len(trans) > idx:
results.append(trans[idx])
else:
results.append(default)
return results
else:
trans = extract_geometry_transforms(geom)
if isinstance(trans, tuple) and len(trans) > idx:
return trans[idx]
return default
@register_class
class SLICENODE_DropGeometry(SliceNode):
bl_label = "Drop Geometry"
list_socket_mapping = [("Mesh", ["Mesh", "Translation"])]
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) -> Any:
def _drop_item(item: Any, target_z: Any) -> tuple[Any, tuple[float, float, float]]:
if not item or getattr(item, "min_z", None) is None:
return item, (0.0, 0.0, 0.0)
z_float = float(target_z if target_z is not None else 0.0)
min_z = round(item.min_z, 6)
offset_z = float(z_float - min_z)
if isinstance(item, SlicingGroup):
copied_data = _copy_slicing_group(item)
else:
copied_data = _copy_slicing_collection(item)
drop_vec = np.array([0.0, 0.0, offset_z], dtype=np.float64)
copied_data.offset(drop_vec)
copied_data.transforms = update_transform(getattr(copied_data, "transforms", []), t_arr=drop_vec)
if isinstance(copied_data, SlicingGroup):
for col in copied_data.collections.values():
col.transforms = update_transform(getattr(col, "transforms", []), t_arr=drop_vec)
for so in col.objects:
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
elif isinstance(copied_data, SlicingCollection):
for so in copied_data.objects:
so.transforms = update_transform(getattr(so, "transforms", []), t_arr=drop_vec)
return copied_data, (0.0, 0.0, offset_z)
return self.eval_multivalue(
_drop_item,
main_socket="Mesh",
secondary_sockets=["Z"],
socket_name=socket_name,
default=(0.0, 0.0, 0.0) if socket_name == "Translation" else None
)
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"
list_socket_mapping = [("Geometry", "Geometry")]
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:
return self.eval_multivalue(
lambda g, name: _rename_slicing_item(g, str(name)) if name else g,
main_socket="Geometry",
secondary_sockets=["Name"],
socket_name=socket_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"
list_socket_mapping = [("Geometry", "Geometry")]
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:
lc = (float(self.line_color[0]), float(self.line_color[1]), float(self.line_color[2]))
sn = bool(self.show_name)
def _assign_one(g: Any, line_w: Any) -> Any:
w = float(line_w) if line_w is not None else float(self.line_width)
return _assign_pdf_properties_to_item(g, w, lc, sn)
return self.eval_multivalue(_assign_one, "Geometry", ["Line Width"], socket_name=socket_name)
def _parse_settings_props(raw_settings: Any) -> list[tuple[str, str]]:
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 props
def _is_single_settings_spec(val: Any) -> bool:
if isinstance(val, dict):
return True
if isinstance(val, (list, tuple)):
if len(val) > 0 and all(isinstance(x, tuple) and len(x) == 2 and not isinstance(x[0], (list, dict, tuple)) for x in val):
return True
if len(val) == 0:
return True
return False
return True
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(geom, props)
return copied
return geom
@register_class
class SLICENODE_AssignPropertyToObject(SliceNode):
bl_label = "Assign PDF Object Properties"
list_socket_mapping = [("Geometry", "Geometry")]
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:
def _assign_one(g: Any, s: Any) -> Any:
props = _parse_settings_props(s)
return _assign_properties_to_item(g, props)
return self.eval_multivalue(_assign_one, "Geometry", ["Settings"], socket_name=socket_name)