334 lines
12 KiB
Python
334 lines
12 KiB
Python
from __future__ import annotations
|
|
from typing import Any
|
|
import numpy as np
|
|
|
|
from ..bpy_ns_types import NfEnumProperty, notify_tree_changed
|
|
from ....registry import register_class
|
|
from .base import SliceNode, SlicePaths, SlicingObject, SlicingCollection, slice_at_range, SliceNodeJoiner
|
|
|
|
@register_class
|
|
class SLICENODE_GetPaths(SliceNode):
|
|
bl_label = "Get Paths"
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Object")
|
|
self.add_input("SLICESOCK_String", "Extrusion Multiplier Attribute").value = 'e_mult'
|
|
self.add_input("SLICESOCK_String", "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)
|
|
|
|
return SlicePaths(so, extrusion_rate=e_rates, feedrate=f_rates, extra_attributes=extra_attrs_data)
|
|
|
|
@register_class
|
|
class SLICENODE_SliceShells(SliceNode):
|
|
bl_label = "Slice Shells"
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Mesh")
|
|
self.add_input("SLICESOCK_Float", "Layer Height").value = 0.2
|
|
self.add_input("SLICESOCK_VectorRotation", "Rotation")
|
|
self.add_output("SLICESOCK_Paths")
|
|
|
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
|
if socket_name != "Paths": return SlicePaths()
|
|
|
|
coll: SlicingCollection | None = self.resolve("Mesh")
|
|
layer_height: float = self.resolve("Layer Height")
|
|
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
|
|
|
|
if not coll: return SlicePaths()
|
|
|
|
merged_object = SlicingObject.merge(coll.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()
|
|
|
|
z_values = np.arange(min_z, max_z + layer_height, layer_height)
|
|
|
|
slices_group: list[list[np.ndarray]] | None = slice_at_range([merged_object.mesh], z_values)
|
|
assert slices_group
|
|
slices = SlicePaths(slices_group[0])
|
|
|
|
if rotation and any(rotation):
|
|
from mathutils import Euler
|
|
mat_inv = np.array(Euler(rotation).to_matrix())
|
|
slices = slices.transform(mat_inv)
|
|
|
|
return slices
|
|
|
|
@register_class
|
|
class SLICENODE_BufferShells(SliceNode):
|
|
bl_label = "Buffer Shells"
|
|
|
|
shape_option: NfEnumProperty(
|
|
name="Miter",
|
|
items=[
|
|
('round', "Round", ""),
|
|
('square', "Square", ""),
|
|
('flat', "Flat", "")
|
|
]
|
|
) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
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_PathOffset(SliceNode):
|
|
bl_label = "Path Offset"
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Paths", "Paths")
|
|
self.add_input("SLICESOCK_Vector", "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_BufferShellsMultiple(SliceNode):
|
|
bl_label = "Buffer Shells Multiple"
|
|
|
|
shape_option: NfEnumProperty(
|
|
name="Miter",
|
|
items=[
|
|
('round', "Round", ""),
|
|
('square', "Square", ""),
|
|
('flat', "Flat", "")
|
|
]
|
|
) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Paths")
|
|
self.add_input("SLICESOCK_Float", "Distance").value = 0
|
|
self.add_input("SLICESOCK_Float", "Width Step").value = 0.4
|
|
self.add_input("SLICESOCK_Int", "Count").value = 2
|
|
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")
|
|
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()
|
|
|
|
all_buffers: list[SlicePaths] = []
|
|
|
|
for i in range(count):
|
|
dist = base_distance + (i * width_step)
|
|
all_buffers.append(paths.buffer(dist, miter=self.shape_option, rotation=rotation))
|
|
|
|
# Zip Logic: Interleave paths by group (Z-layer)
|
|
all_groups = set()
|
|
grouped_inputs = []
|
|
for p_list in all_buffers:
|
|
d = {}
|
|
for i, g in enumerate(p_list.groups):
|
|
all_groups.add(g)
|
|
d.setdefault(g, []).append(p_list[i])
|
|
grouped_inputs.append(d)
|
|
|
|
sorted_groups = sorted(list(all_groups))
|
|
|
|
merged = SlicePaths()
|
|
for g in sorted_groups:
|
|
for d in grouped_inputs:
|
|
group_paths = d.get(g, [])
|
|
for p in group_paths:
|
|
merged.append_with_group(p, g)
|
|
|
|
return paths.copy_with(merged, merged.groups)
|
|
|
|
@register_class
|
|
class SLICENODE_Infill(SliceNode):
|
|
bl_label = "Infill"
|
|
|
|
infill_type: NfEnumProperty(
|
|
name="Type",
|
|
items=[
|
|
('GRID', "Grid", "Cross-hatch grid"),
|
|
('RECTILINEAR', "Rectilinear", "Alternating parallel lines"),
|
|
],
|
|
default='GRID',
|
|
update=notify_tree_changed
|
|
) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
def init(self, context):
|
|
self.add_input("SLICESOCK_Paths")
|
|
self.add_input("SLICESOCK_Float", "Density").value = 2.0
|
|
self.add_input("SLICESOCK_Float", "Infill Rotation").value = 0.0
|
|
self.add_input("SLICESOCK_VectorRotation", "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_JoinSlices(SliceNodeJoiner):
|
|
bl_label = "Boolean Paths"
|
|
socket_type = "SLICESOCK_Paths"
|
|
|
|
operation: 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'
|
|
) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
def draw_buttons(self, context, layout):
|
|
assert layout
|
|
layout.prop(self, "operation", text="")
|
|
|
|
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
|
|
|
|
# Boolean operation loses specific per-segment grouping as it merges polygons.
|
|
# We return a new SlicePaths from the result.
|
|
accumulated = accumulated.boolean(current, self.operation)
|
|
|
|
pass
|
|
|
|
return accumulated
|
|
|
|
@register_class
|
|
class SLICENODE_JoinPaths(SliceNodeJoiner):
|
|
bl_label = "Join Paths"
|
|
socket_type = "SLICESOCK_Paths"
|
|
|
|
mode: NfEnumProperty(
|
|
name="Mode",
|
|
items=[
|
|
('EXTEND', "Extend", "Append paths sequentially"),
|
|
('ZIP', "Zip", "Interleave paths from all inputs"),
|
|
],
|
|
default='EXTEND'
|
|
) # pyright: ignore[reportInvalidTypeForm]
|
|
|
|
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 = resolve_input(input)
|
|
if paths:
|
|
inputs_data.append(paths)
|
|
|
|
if not inputs_data:
|
|
return SlicePaths()
|
|
|
|
if self.mode == 'EXTEND':
|
|
merged = SlicePaths()
|
|
for paths in inputs_data:
|
|
merged.extend_with_groups(paths)
|
|
return merged
|
|
|
|
elif self.mode == 'ZIP':
|
|
# Group-based Zip: sequentially add each group from all inputs.
|
|
# E.g. Group 1 (In1), Group 1 (In2), Group 2 (In1), Group 2 (In2)...
|
|
all_groups = set()
|
|
grouped_inputs = []
|
|
for p_list in inputs_data:
|
|
d = {}
|
|
for i, g in enumerate(p_list.groups):
|
|
all_groups.add(g)
|
|
d.setdefault(g, []).append(p_list[i])
|
|
grouped_inputs.append(d)
|
|
|
|
sorted_groups = sorted(list(all_groups))
|
|
|
|
merged = SlicePaths()
|
|
for g in sorted_groups:
|
|
for d in grouped_inputs:
|
|
group_paths = d.get(g, [])
|
|
for p in group_paths:
|
|
merged.append_with_group(p, g)
|
|
return merged
|
|
|
|
return SlicePaths()
|