911 lines
34 KiB
Python
911 lines
34 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
|
|
|
|
@register_class
|
|
class SLICENODE_GetMesh(SliceNode):
|
|
bl_label = "Get Mesh"
|
|
|
|
include_children: NfBoolProperty(name="Include Children", default=True) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Object")
|
|
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: 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
|
|
) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Mesh")
|
|
self.add_input("SLICESOCK_String", "Attribute")
|
|
self.add_input("SLICESOCK_Float", "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: NfIntProperty(name="Start", default=0, update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm]
|
|
end_val: NfIntProperty(name="End", default=1, update=notify_tree_changed) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Mesh")
|
|
self.add_input("SLICESOCK_String", "Attribute")
|
|
self._update_outputs()
|
|
|
|
def update(self):
|
|
super().update()
|
|
self._update_outputs()
|
|
|
|
def _update_outputs(self):
|
|
# Ensure end >= start
|
|
start = self.start_val
|
|
end = max(start, self.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}")
|
|
|
|
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.all(mask):
|
|
res.objects.append(so)
|
|
elif np.any(mask):
|
|
# Split the object
|
|
so_new = SlicingObject()
|
|
so_new.name = f"{so.name}_{target_val}"
|
|
so_new.parent = so.parent
|
|
so_new.object_type = so.object_type
|
|
so_new.extruder = so.extruder
|
|
so_new.mesh = so.mesh[mask]
|
|
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: 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
|
|
) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
allow_rotations: NfBoolProperty(
|
|
name="Allow Rotations",
|
|
default=True,
|
|
update=notify_tree_changed
|
|
) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Mesh", "Slicing Group", display_shape='LIST')
|
|
self.add_input("SLICESOCK_Float", "Bed X").value = 250.0
|
|
self.add_input("SLICESOCK_Float", "Bed Y").value = 250.0
|
|
self.add_input("SLICESOCK_Float", "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.object_properties = list(getattr(so, "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_slice_paths(sp: SlicePaths, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray) -> SlicePaths:
|
|
new_paths = []
|
|
has_scale = any(s != 1.0 for s in s_arr)
|
|
has_rot = any(r != 0.0 for r in r_arr)
|
|
has_trans = any(t != 0.0 for t in t_arr)
|
|
|
|
if not (has_scale or has_rot or has_trans):
|
|
return sp.copy_with(paths=list(sp))
|
|
|
|
R = None
|
|
if has_rot:
|
|
rx, ry, rz = r_arr
|
|
cx, sx = np.cos(rx), np.sin(rx)
|
|
cy, sy = np.cos(ry), np.sin(ry)
|
|
cz, sz = np.cos(rz), np.sin(rz)
|
|
|
|
Rx = np.array([
|
|
[1.0, 0.0, 0.0],
|
|
[0.0, cx, -sx],
|
|
[0.0, sx, cx]
|
|
], dtype=np.float64)
|
|
Ry = np.array([
|
|
[cy, 0.0, sy],
|
|
[0.0, 1.0, 0.0],
|
|
[-sy, 0.0, cy]
|
|
], dtype=np.float64)
|
|
Rz = np.array([
|
|
[cz, -sz, 0.0],
|
|
[sz, cz, 0.0],
|
|
[0.0, 0.0, 1.0]
|
|
], dtype=np.float64)
|
|
R = Rz @ (Ry @ Rx)
|
|
|
|
for p in sp:
|
|
if len(p) == 0:
|
|
new_paths.append(p.copy())
|
|
continue
|
|
|
|
new_p = p.astype(np.float64, copy=True)
|
|
dim = new_p.shape[1] if new_p.ndim > 1 else 0
|
|
|
|
if has_scale and dim > 0:
|
|
if dim >= 3:
|
|
new_p[:, :3] *= s_arr
|
|
else:
|
|
new_p[:, :dim] *= s_arr[:dim]
|
|
|
|
if has_rot and R is not None and dim > 0:
|
|
if dim >= 3:
|
|
new_p[:, :3] = new_p[:, :3] @ R.T
|
|
elif dim == 2:
|
|
rot_2d = R[:2, :2]
|
|
new_p[:, :2] = new_p[:, :2] @ rot_2d.T
|
|
|
|
if has_trans and dim > 0:
|
|
if dim >= 3:
|
|
new_p[:, :3] += t_arr
|
|
else:
|
|
new_p[:, :dim] += t_arr[:dim]
|
|
|
|
new_paths.append(new_p)
|
|
|
|
return sp.copy_with(paths=new_paths)
|
|
|
|
|
|
def _transform_geometry_item(geom: Any, t_arr: np.ndarray, r_arr: np.ndarray, s_arr: np.ndarray, t_rec: dict) -> 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("SLICESOCK_Vector", "Translation")
|
|
self.add_input("SLICESOCK_VectorRotation", "Rotation")
|
|
self.add_input("SLICESOCK_Vector", "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", "Mesh"):
|
|
return None
|
|
|
|
geom_data: Any = self.resolve("Geometry")
|
|
if geom_data is None:
|
|
geom_data = self.resolve("Mesh")
|
|
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("SLICESOCK_Float", "Z")
|
|
self.add_output("SLICESOCK_Mesh", "Mesh")
|
|
self.add_output("SLICESOCK_VectorRo", "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: 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)
|
|
|
|
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:
|
|
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, (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()):
|
|
if coll.objects:
|
|
coll_name = format_name_template(new_name, coll.objects[0], default_old_name=old_key)
|
|
else:
|
|
coll_name = format_name_template(new_name, None, default_old_name=old_key)
|
|
if len(old_colls) > 1 and "{" not in new_name:
|
|
coll_name = f"{coll_name}_{i}"
|
|
coll.name = coll_name
|
|
for j, obj in enumerate(coll.objects):
|
|
obj_name = format_name_template(new_name, obj, default_old_name=obj.name)
|
|
if (len(coll.objects) > 1 or len(old_colls) > 1) and "{" not in new_name:
|
|
obj_name = f"{obj_name}_{j}"
|
|
obj.name = obj_name
|
|
obj.parent = coll_name
|
|
new_colls[coll_name] = coll
|
|
copied.collections = new_colls
|
|
return copied
|
|
elif isinstance(geom, SlicingCollection):
|
|
copied = _copy_slicing_collection(geom)
|
|
old_coll_name = geom.name
|
|
if copied.objects:
|
|
coll_name = format_name_template(new_name, copied.objects[0], default_old_name=old_coll_name)
|
|
else:
|
|
coll_name = format_name_template(new_name, None, default_old_name=old_coll_name)
|
|
copied.name = coll_name
|
|
for i, obj in enumerate(copied.objects):
|
|
obj_name = format_name_template(new_name, obj, default_old_name=obj.name)
|
|
if len(copied.objects) > 1 and "{" not in new_name:
|
|
obj_name = f"{obj_name}_{i}"
|
|
obj.name = obj_name
|
|
obj.parent = coll_name
|
|
return copied
|
|
elif isinstance(geom, SlicingObject):
|
|
copied = _copy_slicing_object(geom)
|
|
copied.name = format_name_template(new_name, copied, default_old_name=geom.name)
|
|
return copied
|
|
return geom
|
|
|
|
|
|
@register_class
|
|
class SLICENODE_RenameMesh(SliceNode):
|
|
bl_label = "Rename Mesh"
|
|
|
|
new_name: NfStringProperty(name="Name", default="RenamedMesh") # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Mesh", "Geometry")
|
|
self.add_input("SLICESOCK_String", "Name")
|
|
self.add_output("SLICESOCK_Mesh", "Geometry")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
assert layout
|
|
layout.prop(self, "new_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
|
|
|
|
name_socket: str | None = self.resolve("Name")
|
|
target_name = name_socket if name_socket else self.new_name
|
|
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], show_name: bool = False) -> 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)
|
|
for coll in copied.collections.values():
|
|
for obj in coll.objects:
|
|
obj.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name)
|
|
return copied
|
|
elif isinstance(geom, SlicingCollection):
|
|
copied = _copy_slicing_collection(geom)
|
|
for obj in copied.objects:
|
|
obj.pdf_properties = PdfProperties(line_width=line_width, line_color=line_color, show_name=show_name)
|
|
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: NfFloatProperty(name="Line Width (mm)", default=0.0, min=0.0, max=50.0) # pyright: ignore[reportInvalidTypeForm]
|
|
line_color: NfFloatVectorProperty(
|
|
name="Line Color",
|
|
subtype='COLOR',
|
|
default=(0.0, 0.0, 0.0),
|
|
min=0.0,
|
|
max=1.0,
|
|
size=3
|
|
) # pyright: ignore[reportInvalidTypeForm]
|
|
show_name: NfBoolProperty(name="Show Name", default=False) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Mesh", "Geometry")
|
|
self.add_input("SLICESOCK_Float", "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.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.object_properties = resolve_object_properties(obj, props)
|
|
return copied
|
|
elif isinstance(geom, SlicingCollection):
|
|
copied = _copy_slicing_collection(geom)
|
|
for obj in copied.objects:
|
|
obj.object_properties = resolve_object_properties(obj, props)
|
|
return copied
|
|
elif isinstance(geom, SlicingObject):
|
|
copied = _copy_slicing_object(geom)
|
|
copied.object_properties = resolve_object_properties(copied, props)
|
|
return copied
|
|
return geom
|
|
|
|
|
|
@register_class
|
|
class SLICENODE_AssignPropertyToObject(SliceNode):
|
|
bl_label = "Assign Property to Object"
|
|
|
|
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)
|
|
|
|
|
|
|