576 lines
19 KiB
Python
576 lines
19 KiB
Python
from typing import Any, cast, Literal
|
|
import numpy as np
|
|
|
|
from ..bpy_ns_types import NfEnumProperty, NfBoolProperty, notify_tree_changed
|
|
from ....registry import register_class
|
|
from .base import SliceNode, SlicePaths, SlicingObject, SlicingCollection, slice_at_range, SliceNodeJoiner, SlicingGroup
|
|
|
|
@register_class
|
|
class SLICENODE_GetPaths(SliceNode):
|
|
bl_label = "Get 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) -> SlicePaths:
|
|
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
|
|
so, e_rates, f_rates, extra_attrs_data = capture_mesh_edges(obj, e_attr_name, f_attr_name, extra_attrs)
|
|
|
|
sp = SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
|
if hasattr(obj, "transforms"):
|
|
sp.transforms = list(getattr(obj, "transforms", []))
|
|
if hasattr(obj, "name") and getattr(obj, "name"):
|
|
name_val = str(getattr(obj, "name"))
|
|
sp.name = name_val
|
|
sp.objects_info = {"objects": [{"name": name_val, "polygon": []}]}
|
|
return sp
|
|
|
|
@register_class
|
|
class SLICENODE_SliceShells(SliceNode):
|
|
bl_label = "Slice Shells"
|
|
|
|
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) -> SlicePaths:
|
|
if socket_name != "Paths": return SlicePaths()
|
|
|
|
coll: SlicingCollection | SlicingGroup | None = self.resolve("Mesh")
|
|
layer_height: float = self.resolve("Layer Height") or 0.2
|
|
fl_height: float | None = None
|
|
if hasattr(self, "inputs") and "First Layer Height" in self.inputs:
|
|
try:
|
|
fl_height = self.resolve("First Layer Height")
|
|
except Exception:
|
|
fl_height = None
|
|
else:
|
|
try:
|
|
fl_height = self.resolve("First Layer Height")
|
|
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()
|
|
|
|
# Handle SlicingGroup flattening
|
|
if isinstance(coll, SlicingGroup):
|
|
objects = []
|
|
for c in coll.collections.values():
|
|
objects.extend(c.objects)
|
|
else:
|
|
objects = coll.objects
|
|
|
|
merged_object = SlicingObject.merge(objects)
|
|
|
|
if rotation and any(rotation):
|
|
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 = merged_object.mesh[:,0:3,2:3].min()
|
|
from math import floor
|
|
min_z = floor(min_z / layer_height) * layer_height
|
|
max_z = merged_object.mesh[:,0:3,2:3].max()
|
|
|
|
h_first = fl_height if fl_height is not None else layer_height
|
|
z_first = min_z + h_first
|
|
z_rest_start = min_z + h_first + layer_height
|
|
|
|
if z_rest_start < max_z:
|
|
z_rest = np.arange(z_rest_start, max_z, layer_height)
|
|
z_values = np.concatenate(([z_first], z_rest))
|
|
elif z_first < max_z:
|
|
z_values = np.array([z_first])
|
|
else:
|
|
z_values = np.array([])
|
|
|
|
slices_group: list[list[np.ndarray]] | None = slice_at_range(
|
|
[merged_object.mesh],
|
|
z_values,
|
|
ignore_holes=ignore_holes
|
|
)
|
|
assert slices_group
|
|
slices = SlicePaths(slices_group[0])
|
|
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 = "-".join(names)
|
|
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 = z_values[0]
|
|
for i, p in enumerate(slices):
|
|
if len(p) > 0 and abs(p[0, 2] - z_first) < 1e-4:
|
|
slices.extrusion_rate[i] = slices.extrusion_rate[i] * mult
|
|
|
|
if rotation and any(rotation):
|
|
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_BufferShells(SliceNode):
|
|
bl_label = "Buffer Shells"
|
|
|
|
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("SLICESOCK_Float", "Distance").value = 0
|
|
self.add_input("SLICESOCK_VectorRotation", "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) -> SlicePaths:
|
|
if socket_name != "Paths": return SlicePaths()
|
|
|
|
paths: SlicePaths = self.resolve("Paths")
|
|
distance: float = self.resolve("Distance") or 0.0
|
|
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
|
|
|
if not paths: return SlicePaths()
|
|
|
|
return paths.buffer(distance, miter=self.shape_option, rotation=rotation)
|
|
|
|
@register_class
|
|
class SLICENODE_BufferCurve(SliceNode):
|
|
bl_label = "Buffer Curve"
|
|
|
|
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) -> SlicePaths:
|
|
if socket_name != "Paths": return SlicePaths()
|
|
|
|
paths: SlicePaths = self.resolve("Paths")
|
|
distance: float = self.resolve("Distance") or 0.0
|
|
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
|
|
|
if not paths: return SlicePaths()
|
|
|
|
return paths.buffer_curve(
|
|
distance,
|
|
cap_style=self.cap_style,
|
|
join_style=self.join_style,
|
|
single_sided=self.single_sided,
|
|
rotation=rotation
|
|
)
|
|
|
|
@register_class
|
|
class SLICENODE_OffsetCurve(SliceNode):
|
|
bl_label = "Offset Curve"
|
|
|
|
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) -> SlicePaths:
|
|
if socket_name != "Paths": return SlicePaths()
|
|
|
|
paths: SlicePaths = self.resolve("Paths")
|
|
distance: float = self.resolve("Distance") or 0.0
|
|
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
|
|
|
if not paths: return SlicePaths()
|
|
|
|
return paths.offset_curve(
|
|
distance,
|
|
join_style=cast(Literal['round', 'mitre'], self.join_style),
|
|
rotation=rotation
|
|
)
|
|
|
|
@register_class
|
|
class SLICENODE_PathOffset(SliceNode):
|
|
bl_label = "Path Offset"
|
|
|
|
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: SlicePaths = self.resolve("Paths")
|
|
offset: Any = self.resolve("Offset") or (0.0, 0.0, 0.0)
|
|
|
|
if not paths: return SlicePaths()
|
|
|
|
return paths.offset(np.array(offset, dtype=np.float32))
|
|
|
|
@register_class
|
|
class SLICENODE_RotatePath(SliceNode):
|
|
bl_label = "Rotate Path"
|
|
|
|
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: SlicePaths = self.resolve("Paths")
|
|
angle: float = self.resolve("Angle") or 0.0
|
|
center: Any = self.resolve("Center")
|
|
|
|
if not paths: return SlicePaths()
|
|
|
|
center_vec = np.array(center, dtype=np.float32) if center and any(center) else None
|
|
|
|
return paths.rotate(angle, center=center_vec)
|
|
|
|
@register_class
|
|
class SLICENODE_BufferShellsMultiple(SliceNode):
|
|
bl_label = "Buffer Shells Multiple"
|
|
|
|
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")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
assert layout
|
|
layout.prop(self, "shape_option")
|
|
|
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
|
if socket_name != "Paths": return SlicePaths()
|
|
|
|
paths: SlicePaths = self.resolve("Paths")
|
|
base_distance: float = self.resolve("Distance") or 0.0
|
|
width_step: float = self.resolve("Width Step") or 0.0
|
|
count: int = self.resolve("Count") or 1
|
|
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
|
|
|
if not paths: return SlicePaths()
|
|
|
|
return paths.buffer_multiple(base_distance, count, width_step, miter=self.shape_option, rotation=rotation)
|
|
|
|
@register_class
|
|
class SLICENODE_Infill(SliceNode):
|
|
bl_label = "Infill"
|
|
|
|
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) -> SlicePaths:
|
|
if socket_name != "Paths": return SlicePaths()
|
|
|
|
paths: SlicePaths = self.resolve("Paths")
|
|
density: float = self.resolve("Density") or 2.0
|
|
infill_rotation: float = self.resolve("Infill Rotation") or 0.0
|
|
rotation: Any = self.resolve("Layer Rotation") or (0.0, 0.0, 0.0)
|
|
|
|
if not paths: return SlicePaths()
|
|
|
|
return paths.infill(
|
|
density,
|
|
rotation_deg=infill_rotation,
|
|
infill_type=self.infill_type, # pyright: ignore[reportOptionalMemberAccess]
|
|
layer_rotation=rotation
|
|
)
|
|
|
|
@register_class
|
|
class SLICENODE_GetInfillMask(SliceNode):
|
|
bl_label = "Get Infill Mask"
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Paths")
|
|
self.add_output("SLICESOCK_Paths")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
pass
|
|
|
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
|
if socket_name != "Paths": return SlicePaths()
|
|
|
|
paths: SlicePaths = self.resolve("Paths")
|
|
|
|
if not paths: return SlicePaths()
|
|
|
|
return paths.get_infill_mask()
|
|
|
|
@register_class
|
|
class SLICENODE_OverhangMask(SliceNode):
|
|
bl_label = "Overhang Mask"
|
|
|
|
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) -> SlicePaths:
|
|
if socket_name != "Paths": return SlicePaths()
|
|
|
|
paths: SlicePaths = self.resolve("Paths")
|
|
tolerance: float = self.resolve("Tolerance") or 0.05
|
|
|
|
if not paths: return SlicePaths()
|
|
|
|
return paths.get_overhang_mask(tolerance=tolerance)
|
|
|
|
@register_class
|
|
class SLICENODE_JoinSlices(SliceNodeJoiner):
|
|
bl_label = "Boolean Paths"
|
|
socket_type = "SLICESOCK_Paths"
|
|
|
|
operation: str = NfEnumProperty(
|
|
name="Operation",
|
|
items=[
|
|
('UNION', "Union", "Union of the slices"),
|
|
('INTERSECTION', "Intersection", "Intersection of the slices"),
|
|
('DIFFERENCE', "Subtract", "Clipping of the first slice"),
|
|
('XOR', "Exclusive or", "Exclusive or of the slices"),
|
|
],
|
|
default='UNION'
|
|
)
|
|
|
|
connect_along_boundary: bool = NfBoolProperty(
|
|
name="Connect along boundary",
|
|
description="Connect clipped segments along the boundary of the clipping polygon",
|
|
default=False
|
|
)
|
|
|
|
def draw_buttons(self, context, layout):
|
|
assert layout
|
|
layout.prop(self, "operation", text="")
|
|
if self.operation in ['INTERSECTION', 'DIFFERENCE']:
|
|
layout.prop(self, "connect_along_boundary")
|
|
|
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
|
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
|
accumulated: SlicePaths = SlicePaths()
|
|
current: SlicePaths | None = SlicePaths()
|
|
for input in self.inputs:
|
|
current = resolve_input(input)
|
|
|
|
if not current:
|
|
continue
|
|
|
|
if not accumulated:
|
|
accumulated = current
|
|
continue
|
|
|
|
accumulated = accumulated.boolean(current, self.operation, connect_along_boundary=self.connect_along_boundary)
|
|
|
|
return accumulated
|
|
|
|
@register_class
|
|
class SLICENODE_JoinPaths(SliceNodeJoiner):
|
|
bl_label = "Join Paths"
|
|
socket_type = "SLICESOCK_Paths"
|
|
|
|
mode: str = NfEnumProperty(
|
|
name="Mode",
|
|
items=[
|
|
('EXTEND', "Extend", "Append paths sequentially"),
|
|
('ZIP', "Zip", "Interleave paths from all inputs"),
|
|
],
|
|
default='EXTEND'
|
|
)
|
|
|
|
def draw_buttons(self, context, layout):
|
|
assert layout
|
|
layout.prop(self, "mode", text="")
|
|
|
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
|
from ....infra.blender.blender_slice_nodes_generic import resolve_input
|
|
|
|
inputs_data: list[SlicePaths] = []
|
|
for input in self.inputs:
|
|
paths: SlicePaths | None = resolve_input(input)
|
|
if paths:
|
|
inputs_data.append(paths)
|
|
|
|
if not inputs_data:
|
|
return SlicePaths()
|
|
|
|
if self.mode == 'EXTEND':
|
|
return SlicePaths.extend_paths(inputs_data)
|
|
elif self.mode == 'ZIP':
|
|
return SlicePaths.zip_paths(inputs_data)
|
|
|
|
return SlicePaths()
|
|
|
|
|
|
@register_class
|
|
class SLICENODE_SimplifyPaths(SliceNode):
|
|
bl_label = "Simplify 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) -> SlicePaths:
|
|
if socket_name != "Paths":
|
|
return SlicePaths()
|
|
|
|
paths: SlicePaths = self.resolve("Paths")
|
|
tolerance: float = self.resolve("Tolerance") or 0.0
|
|
if not paths or tolerance <= 0:
|
|
return paths
|
|
|
|
return paths.simplify(tolerance, preserve_topology=self.preserve_topology)
|