nodeslicer/core/nodeslicer/nodes/path.py
2026-06-10 16:05:51 +03:00

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()