nodeslicer/core/nodeslicer/nodes/path.py

836 lines
29 KiB
Python

import time
from typing import Any, cast, Literal
import numpy as np
from ..bpy_ns_types import NfEnumProperty, NfBoolProperty, NfFloatProperty, notify_tree_changed
from ....registry import register_class
from .base import SliceNode, SlicePaths, SlicingObject, SlicingCollection, slice_at_range, SliceNodeJoiner, SliceNodeListJoiner, SlicingGroup
from ...slice import align_path_seam_sharpest, is_closed_path
from ....infra.blender.list_utils import eval_element_wise, eval_element_wise_multivalue, prepare_indexed_inputs, is_list_data, is_input_a_list, set_socket_shape, is_socket_linked_to_list, flatten_list
@register_class
class SLICENODE_PathsToList(SliceNodeListJoiner):
bl_label = "Paths to List"
socket_type = "SLICESOCK_Paths"
socket_label = "Paths"
@register_class
class SLICENODE_GetPaths(SliceNode):
bl_label = "Get Paths"
list_socket_mapping = [("Object", "Paths")]
def init(self, context):
self.add_input("NodeSocketObject")
self.add_input("NodeSocketString", "Extrusion Multiplier Attribute").value = 'e_mult'
self.add_input("NodeSocketString", "Feedrate Attribute").value = 'f_rate'
self.add_input("SLICESOCK_Settings", "Extra Attributes")
self.add_output("SLICESOCK_Paths")
def draw_buttons(self, context, layout):
pass
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
obj: Any = self.resolve("Object")
e_attr_name: str = self.resolve("Extrusion Multiplier Attribute")
f_attr_name: str = self.resolve("Feedrate Attribute")
extra_attrs: dict[str, str] = self.resolve("Extra Attributes")
from ....infra.blender.blender_mesh_capture import capture_mesh_edges
def _get_single_path(o: Any, e_name: Any, f_name: Any, extra: Any) -> SlicePaths:
if not o:
return SlicePaths()
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(o, str(e_name or ""), str(f_name or ""), extra)
sp = SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
if hasattr(o, "transforms"):
sp.transforms = list(getattr(o, "transforms", []))
if hasattr(o, "name") and getattr(o, "name"):
name_val = str(getattr(o, "name"))
sp.name = name_val
sp.objects_info = {"objects": [{"name": name_val, "polygon": []}]}
return sp
return eval_element_wise_multivalue(
_get_single_path,
obj,
e_attr_name,
f_attr_name,
extra_attrs,
input_names=["Extrusion Multiplier Attribute", "Feedrate Attribute", "Extra Attributes"]
)
def _slice_single_mesh_item(coll: Any, layer_height: float, fl_height: float | None, rotation: Any, ignore_holes: bool) -> SlicePaths:
if not coll:
return SlicePaths()
# Handle SlicingGroup, SlicingCollection, SlicingObject
if isinstance(coll, SlicingGroup):
objects = []
for c in coll.collections.values():
objects.extend(c.objects)
elif isinstance(coll, SlicingCollection):
objects = list(coll.objects)
elif isinstance(coll, SlicingObject):
objects = [coll]
else:
objects = getattr(coll, "objects", [coll] if hasattr(coll, "mesh") else [])
merged_object = SlicingObject.merge(objects)
if merged_object.mesh is None or merged_object.mesh.size == 0:
return SlicePaths()
has_rot = bool(rotation and any(rotation))
if has_rot:
from mathutils import Euler
mat = np.array(Euler(rotation).to_matrix().inverted())
merged_object.mesh[:, :3, :] = merged_object.mesh[:, :3, :] @ mat.T
merged_object.mesh[:, 3:4, :] = merged_object.mesh[:, 3:4, :] @ mat.T
min_z = float(merged_object.mesh[:, :3, 2].min())
if abs(min_z) < 1e-6:
min_z = 0.0
max_z = float(merged_object.mesh[:, :3, 2].max())
h_first = fl_height if fl_height is not None else layer_height
min_z_um = int(round(min_z * 1000))
lh_um = int(round(layer_height * 1000))
flh_um = int(round(h_first * 1000))
max_z_um = int(round(max_z * 1000))
z_first_um = min_z_um + flh_um
z_rest_start_um = z_first_um + lh_um
if z_first_um <= max_z_um:
if z_rest_start_um <= max_z_um and lh_um > 0:
z_um_values = np.arange(z_rest_start_um, max_z_um + 1, lh_um, dtype=np.int64)
z_values = np.concatenate(([z_first_um], z_um_values)) / 1000.0
else:
z_values = np.array([z_first_um], dtype=np.float64) / 1000.0
else:
z_values = np.array([], dtype=np.float64)
slices_group: list[list[np.ndarray]] | None = slice_at_range(
[merged_object.mesh],
z_values,
ignore_holes=ignore_holes
)
if not slices_group or not slices_group[0]:
return SlicePaths()
paths = slices_group[0]
z_to_group: dict[int, int] = {}
groups = []
for p in paths:
if len(p) > 0:
z_k = int(round(p[0, 2] * 1000))
if z_k not in z_to_group:
z_to_group[z_k] = len(z_to_group)
groups.append(z_to_group[z_k])
else:
groups.append(0)
slices = SlicePaths(paths, groups=groups)
slices.layer_height = layer_height
if fl_height is not None:
slices.first_layer_height = fl_height
slices.transforms = list(getattr(coll, "transforms", [])) or list(getattr(merged_object, "transforms", []))
names = []
if objects:
for o in objects:
n = getattr(o, "name", "")
if n:
if n.endswith("_merged"):
n = n[:-7]
if n and n not in names:
names.append(n)
if not names and hasattr(coll, "name") and getattr(coll, "name"):
names.append(str(getattr(coll, "name")))
if names:
slices.name = names[0]
slices.objects_info = {"objects": [{"name": n, "polygon": []} for n in names]}
if len(z_values) > 0 and fl_height is not None and layer_height > 0:
mult = fl_height / layer_height
z_first_um = int(round(z_values[0] * 1000))
for i, p in enumerate(slices):
if len(p) > 0 and int(round(p[0, 2] * 1000)) == z_first_um:
slices.extrusion_rate[i] = slices.extrusion_rate[i] * mult
else:
break
if has_rot:
from mathutils import Euler
mat_inv = np.array(Euler(rotation).to_matrix())
slices = slices.transform(mat_inv)
slices = slices.align_seams(mode="sharpest")
return slices
@register_class
class SLICENODE_SliceShells(SliceNode):
bl_label = "Slice Shells"
list_socket_mapping = [("Mesh", "Paths")]
ignore_holes: bool = NfBoolProperty(
name="Ignore Holes",
description="Ignore interior hole boundaries and generate only outer shells",
default=False
)
def init(self, context):
self.add_input("SLICESOCK_Mesh")
self.add_input("NodeSocketFloat", "Layer Height").value = 0.2
self.add_input("NodeSocketFloat", "First Layer Height").value = 0.2
self.add_input("NodeSocketRotation", "Rotation")
self.add_output("SLICESOCK_Paths")
def draw_buttons(self, context, layout):
assert layout
if hasattr(self, "ignore_holes"):
layout.prop(self, "ignore_holes")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
coll: Any = self.resolve("Mesh")
layer_height: float = float(self.resolve("Layer Height") or 0.2)
if layer_height <= 0:
layer_height = 0.2
fl_height: float | None = None
try:
fl_val = self.resolve("First Layer Height")
if fl_val is not None:
fl_height = float(fl_val)
except Exception:
fl_height = None
if fl_height is not None and fl_height <= 0:
fl_height = None
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
ignore_holes: bool = bool(getattr(self, "ignore_holes", False))
if hasattr(self, "inputs") and "Ignore Holes" in self.inputs:
try:
val = self.resolve("Ignore Holes")
if val is not None:
ignore_holes = bool(val)
except Exception:
pass
if not coll: return SlicePaths()
def _slice_one(item: Any, lh_val: Any, flh_val: Any, rot_val: Any) -> SlicePaths:
lh = float(lh_val) if lh_val is not None and float(lh_val) > 0 else 0.2
flh = float(flh_val) if flh_val is not None and float(flh_val) > 0 else None
return _slice_single_mesh_item(item, lh, flh, rot_val, ignore_holes)
return eval_element_wise_multivalue(
_slice_one,
coll,
layer_height,
fl_height,
rotation,
is_vector=[False, False, True],
input_names=["Layer Height", "First Layer Height", "Rotation"]
)
@register_class
class SLICENODE_BufferShells(SliceNode):
bl_label = "Buffer Shells"
list_socket_mapping = [("Paths", "Paths")]
shape_option: str = NfEnumProperty(
name="Miter",
items=[
('round', "Round", ""),
('square', "Square", ""),
('flat', "Flat", "")
]
)
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_input("NodeSocketFloat", "Distance").value = 0
self.add_input("NodeSocketRotation", "Rotation")
self.add_output("SLICESOCK_Paths")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "shape_option")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
paths = self.resolve("Paths")
distance_val = self.resolve("Distance")
distance: float = float(distance_val) if distance_val is not None else 0.0
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
if not paths: return SlicePaths()
def _buf(p: Any, dist: Any, rot: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.buffer(float(dist if dist is not None else 0.0), miter=self.shape_option, rotation=rot)
return eval_element_wise_multivalue(
_buf,
paths,
distance,
rotation,
is_vector=[False, True],
input_names=["Distance", "Rotation"]
)
@register_class
class SLICENODE_BufferCurve(SliceNode):
bl_label = "Buffer Curve"
list_socket_mapping = [("Paths", "Paths")]
cap_style: str = NfEnumProperty(
name="Cap Style",
items=[
('round', "Round", ""),
('flat', "Flat", ""),
('square', "Square", "")
],
default='round'
)
join_style: str = NfEnumProperty(
name="Join Style",
items=[
('round', "Round", ""),
('mitre', "Mitre", ""),
('bevel', "Bevel", "")
],
default='round'
)
single_sided: bool = NfBoolProperty(
name="Single Sided",
description="Buffer only on one side of the curve",
default=False
)
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_input("NodeSocketFloat", "Distance").value = 0.0
self.add_input("NodeSocketRotation", "Rotation")
self.add_output("SLICESOCK_Paths")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "cap_style")
layout.prop(self, "join_style")
layout.prop(self, "single_sided")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
paths = self.resolve("Paths")
distance = self.resolve("Distance")
rotation = self.resolve("Rotation")
if not paths: return SlicePaths()
def _buf(p: Any, dist: Any, rot: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.buffer_curve(
float(dist if dist is not None else 0.0),
cap_style=self.cap_style,
join_style=self.join_style,
single_sided=self.single_sided,
rotation=rot
)
return eval_element_wise_multivalue(
_buf,
paths,
distance,
rotation,
is_vector=[False, True],
input_names=["Distance", "Rotation"]
)
@register_class
class SLICENODE_OffsetCurve(SliceNode):
bl_label = "Offset Curve"
list_socket_mapping = [("Paths", "Paths")]
join_style: str = NfEnumProperty(
name="Join Style",
items=[
('round', "Round", ""),
('mitre', "Mitre", ""),
('bevel', "Bevel", "")
],
default='round'
)
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_input("NodeSocketFloat", "Distance").value = 0.0
self.add_input("NodeSocketRotation", "Rotation")
self.add_output("SLICESOCK_Paths")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "join_style")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
paths = self.resolve("Paths")
distance = self.resolve("Distance")
rotation = self.resolve("Rotation")
if not paths: return SlicePaths()
def _off(p: Any, dist: Any, rot: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.offset_curve(
float(dist if dist is not None else 0.0),
join_style=cast(Literal['round', 'mitre'], self.join_style),
rotation=rot
)
return eval_element_wise_multivalue(
_off,
paths,
distance,
rotation,
is_vector=[False, True],
input_names=["Distance", "Rotation"]
)
@register_class
class SLICENODE_PathOffset(SliceNode):
bl_label = "Path Offset"
list_socket_mapping = [("Paths", "Paths")]
def init(self, context):
self.add_input("SLICESOCK_Paths", "Paths")
self.add_input("NodeSocketVector", "Offset")
self.add_output("SLICESOCK_Paths", "Paths")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
paths = self.resolve("Paths")
offset = self.resolve("Offset")
if not paths: return SlicePaths()
def _off(p: Any, off: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.offset(np.array(off if off is not None else (0.0, 0.0, 0.0), dtype=np.float64))
return eval_element_wise_multivalue(
_off,
paths,
offset,
is_vector=[True],
input_names=["Offset"]
)
@register_class
class SLICENODE_RotatePath(SliceNode):
bl_label = "Rotate Path"
list_socket_mapping = [("Paths", "Paths")]
def init(self, context):
self.add_input("SLICESOCK_Paths", "Paths")
self.add_input("NodeSocketFloat", "Angle").value = 0.0
self.add_input("NodeSocketVector", "Center")
self.add_output("SLICESOCK_Paths", "Paths")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
paths = self.resolve("Paths")
angle = self.resolve("Angle")
center = self.resolve("Center")
if not paths: return SlicePaths()
def _rot(p: Any, ang: Any, cent: Any) -> SlicePaths:
if not p: return SlicePaths()
center_vec = np.array(cent, dtype=np.float64) if cent and any(cent) else None
return p.rotate(float(ang if ang is not None else 0.0), center=center_vec)
return eval_element_wise_multivalue(
_rot,
paths,
angle,
center,
is_vector=[False, True],
input_names=["Angle", "Center"]
)
@register_class
class SLICENODE_BufferShellsMultiple(SliceNode):
bl_label = "Buffer Shells Multiple"
list_socket_mapping = [("Paths", ["Paths", "Outermost Path", "Innermost Path"])]
shape_option: str = NfEnumProperty(
name="Miter",
items=[
('round', "Round", ""),
('square', "Square", ""),
('flat', "Flat", "")
]
)
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_input("NodeSocketFloat", "Distance").value = 0
self.add_input("NodeSocketFloat", "Width Step").value = 0.4
self.add_input("NodeSocketInt", "Count").value = 2
self.add_input("NodeSocketRotation", "Rotation")
self.add_output("SLICESOCK_Paths", "Paths")
self.add_output("SLICESOCK_Paths", "Outermost Path")
self.add_output("SLICESOCK_Paths", "Innermost Path")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "shape_option")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
paths = self.resolve("Paths")
base_distance = self.resolve("Distance")
width_step = self.resolve("Width Step")
count = self.resolve("Count")
rotation = self.resolve("Rotation")
if not paths:
return SlicePaths()
def _buf_multi(p: Any, b_dist: Any, w_step: Any, cnt: Any, rot: Any) -> SlicePaths:
c = int(cnt if cnt is not None else 1)
if not p or c <= 0:
return SlicePaths()
dist = float(b_dist if b_dist is not None else 0.0)
step = float(w_step if w_step is not None else 0.4)
if socket_name == "Outermost Path":
outer_dist = max(dist, dist + (c - 1) * step) if c > 1 else dist
return p.buffer(outer_dist, miter=self.shape_option, rotation=rot)
elif socket_name == "Innermost Path":
inner_dist = min(dist, dist + (c - 1) * step) if c > 1 else dist
return p.buffer(inner_dist, miter=self.shape_option, rotation=rot)
elif socket_name == "Paths":
return p.buffer_multiple(dist, c, step, miter=self.shape_option, rotation=rot)
return SlicePaths()
return eval_element_wise_multivalue(
_buf_multi,
paths,
base_distance,
width_step,
count,
rotation,
is_vector=[False, False, False, True],
input_names=["Distance", "Width Step", "Count", "Rotation"]
)
@register_class
class SLICENODE_Infill(SliceNode):
bl_label = "Infill"
list_socket_mapping = [("Paths", "Paths")]
infill_type: str = NfEnumProperty(
name="Type",
items=[
('GRID', "Grid", "Cross-hatch grid"),
('RECTILINEAR', "Rectilinear", "Alternating parallel lines"),
('LINES', "Lines", "Parallel lines in one direction"),
],
default='GRID',
update=notify_tree_changed
)
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_input("NodeSocketFloat", "Density").value = 2.0
self.add_input("NodeSocketFloat", "Infill Rotation").value = 0.0
self.add_input("NodeSocketRotation", "Layer Rotation")
self.add_output("SLICESOCK_Paths")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "infill_type", text="")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
paths = self.resolve("Paths")
density = self.resolve("Density")
infill_rotation = self.resolve("Infill Rotation")
rotation = self.resolve("Layer Rotation")
if not paths: return SlicePaths()
def _inf(p: Any, dens: Any, inf_rot: Any, rot: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.infill(
float(dens if dens is not None else 2.0),
rotation_deg=float(inf_rot if inf_rot is not None else 0.0),
infill_type=self.infill_type,
layer_rotation=rot
)
return eval_element_wise_multivalue(
_inf,
paths,
density,
infill_rotation,
rotation,
is_vector=[False, False, True],
input_names=["Density", "Infill Rotation", "Layer Rotation"]
)
@register_class
class SLICENODE_OverhangMask(SliceNode):
bl_label = "Overhang Mask"
list_socket_mapping = [("Paths", "Paths")]
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_input("NodeSocketFloat", "Tolerance").value = 0.05
self.add_output("SLICESOCK_Paths")
def draw_buttons(self, context, layout):
pass
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths": return SlicePaths()
paths = self.resolve("Paths")
tolerance = self.resolve("Tolerance")
if not paths: return SlicePaths()
def _mask(p: Any, tol: Any) -> SlicePaths:
if not p: return SlicePaths()
return p.get_overhang_mask(tolerance=float(tol if tol is not None else 0.05))
return eval_element_wise_multivalue(_mask, paths, tolerance, input_names=["Tolerance"])
@register_class
class SLICENODE_PathOperations(SliceNodeJoiner):
bl_label = "Path Operations"
socket_type = "SLICESOCK_Paths"
operation: str = NfEnumProperty(
name="Operation",
items=[
('ZIP', "Zip paths", "Interleave paths from all inputs layer by layer"),
('EXTEND', "Extend paths", "Append paths sequentially"),
('MAX_COVERAGE', "Maximum coverage", "Union of all paths"),
('MIN_COVERAGE', "Minimum coverage", "Intersection of paths per layer (single paths preserved)"),
('INTERSECTION', "Intersection", "Intersection of all paths"),
('SUBTRACT', "Subtract", "Subtract subsequent paths from first input"),
('XOR', "Exclusive or", "Exclusive or of all paths"),
],
default='ZIP',
update=notify_tree_changed
)
connect_along_boundary: bool = NfBoolProperty(
name="Connect along boundary",
description="Connect clipped segments along the boundary of the clipping polygon",
default=False
)
acquire_transforms: bool = NfBoolProperty(
name="Acquire Transforms",
description="Apply transforms from primary path metadata to subsequent paths so they line up as in the viewport",
default=True,
update=notify_tree_changed
)
def update(self) -> None:
super().update()
if self.inputs:
first_inp = self.inputs[0]
is_list = is_socket_linked_to_list(first_inp)
set_socket_shape(first_inp, "LIST" if is_list else "CIRCLE")
for inp in self.inputs[1:]:
set_socket_shape(inp, "LIST" if is_socket_linked_to_list(inp) else "CIRCLE")
if self.outputs:
set_socket_shape(self.outputs[0], "LIST" if is_list else "CIRCLE")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "operation", text="")
if self.operation in ['MIN_COVERAGE', 'INTERSECTION', 'SUBTRACT']:
layout.prop(self, "connect_along_boundary")
layout.prop(self, "acquire_transforms")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
from ....infra.blender.blender_slice_nodes_generic import resolve_input
if not self.inputs:
return SlicePaths()
def _is_valid_paths_input(val: Any) -> bool:
if val is None:
return False
if isinstance(val, SlicePaths):
return len(val) > 0
if is_list_data(val):
flat = flatten_list(val)
return any(isinstance(x, SlicePaths) and len(x) > 0 for x in flat)
return False
first_val = resolve_input(self.inputs[0]) if getattr(self.inputs[0], "is_linked", False) else None
active_other_inputs: list[tuple[str, Any]] = []
for input_socket in self.inputs[1:]:
if getattr(input_socket, "is_linked", False):
val = resolve_input(input_socket)
if _is_valid_paths_input(val):
active_other_inputs.append((getattr(input_socket, "name", "Paths"), val))
acquire_t = bool(getattr(self, "acquire_transforms", True))
if not active_other_inputs:
def _single_op(p: Any) -> Any:
if isinstance(p, SlicePaths):
return p
return SlicePaths()
if first_val is None:
return SlicePaths()
return eval_element_wise(_single_op, first_val)
other_names = [item[0] for item in active_other_inputs]
other_vals = [item[1] for item in active_other_inputs]
is_list, p0_items, other_lists = prepare_indexed_inputs(
first_val,
*other_vals,
input_names=other_names
)
def _run_op_at_index(p0: Any, *others: Any) -> SlicePaths:
valid_others = [p for p in others if isinstance(p, SlicePaths) and len(p) > 0]
inputs_data = [p for p in [p0] + valid_others if isinstance(p, SlicePaths) and len(p) > 0]
if not inputs_data:
return SlicePaths()
return SlicePaths.path_operations(
inputs_data,
operation=self.operation,
connect_along_boundary=self.connect_along_boundary,
acquire_transforms=acquire_t
)
if is_list:
return [
_run_op_at_index(p0_items[i], *(other_lists[k][i] for k in range(len(other_lists))))
for i in range(len(p0_items))
]
return _run_op_at_index(p0_items[0], *(other_lists[k][0] for k in range(len(other_lists))))
@register_class
class SLICENODE_SimplifyPaths(SliceNode):
bl_label = "Simplify Paths"
list_socket_mapping = [("Paths", "Paths")]
preserve_topology: bool = NfBoolProperty(
name="Preserve Topology",
description="Prevents swapping of contained/containing geometry during simplification",
default=True
)
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_input("NodeSocketFloat", "Tolerance").value = 0.1
self.add_output("SLICESOCK_Paths")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "preserve_topology")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths":
return SlicePaths()
paths = self.resolve("Paths")
tolerance = self.resolve("Tolerance")
if not paths:
return SlicePaths()
def _simp(p: Any, tol: Any) -> SlicePaths:
t_val = float(tol if tol is not None else 0.0)
if not p or t_val <= 0:
return p if isinstance(p, SlicePaths) else SlicePaths()
return p.simplify(t_val, preserve_topology=self.preserve_topology)
return eval_element_wise_multivalue(_simp, paths, tolerance, input_names=["Tolerance"])
@register_class
class SLICENODE_SeamPlacement(SliceNode):
bl_label = "Seam Placement"
list_socket_mapping = [("Paths", "Paths")]
mode: str = NfEnumProperty(
name="Mode",
items=[
('CLOSEST', "Closest to Seam", "Reposition seam to intersection nearest to existing seam"),
('FIRST', "First Intersection", "Reposition seam to first intersection along path traversal"),
('CENTROID', "Mesh Centroid", "Reposition seam to intersection nearest to mesh center"),
],
default='CLOSEST',
update=notify_tree_changed
)
acquire_transforms: bool = NfBoolProperty(
name="Acquire Transforms",
description="Apply transforms from paths metadata to the cutting mesh so they line up as in the viewport",
default=True,
update=notify_tree_changed
)
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_input("SLICESOCK_Mesh")
self.add_input("NodeSocketFloat", "Seam Staggering").value = 0.0
self.add_output("SLICESOCK_Paths")
def draw_buttons(self, context, layout):
assert layout
layout.prop(self, "mode", text="")
layout.prop(self, "acquire_transforms")
def evaluate_output(self, socket_name: str, **_kwargs) -> Any:
if socket_name != "Paths":
return SlicePaths()
paths = self.resolve("Paths")
mesh_input: Any = self.resolve("Mesh")
stagger_val = self.resolve("Seam Staggering")
stagger_mm: float = float(stagger_val) if stagger_val is not None else 0.0
if not paths:
return SlicePaths()
acquire_t = bool(getattr(self, "acquire_transforms", True))
def _place(p: Any, m_in: Any, stg: Any) -> SlicePaths:
if not p:
return SlicePaths()
s_val = float(stg if stg is not None else 0.0)
if not m_in and abs(s_val) < 1e-6:
return p
return p.place_seams(m_in, mode=self.mode.lower(), acquire_transforms=acquire_t, stagger_mm=s_val)
return eval_element_wise_multivalue(
_place,
paths,
mesh_input,
stagger_mm,
input_names=["Mesh", "Seam Staggering"]
)