836 lines
29 KiB
Python
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"]
|
|
)
|
|
|
|
|