This commit is contained in:
Nicolas Predella 2026-06-26 13:39:13 +03:00
parent 3c186aae9f
commit dbc631e3dd
5 changed files with 98 additions and 6 deletions

View file

@ -4,7 +4,7 @@ import numpy as np
from ..bpy_ns_types import NfEnumProperty, NfBoolProperty, notify_tree_changed from ..bpy_ns_types import NfEnumProperty, NfBoolProperty, notify_tree_changed
from ....registry import register_class from ....registry import register_class
from .base import SliceNode, SlicePaths, SlicingObject, SlicingCollection, slice_at_range, SliceNodeJoiner from .base import SliceNode, SlicePaths, SlicingObject, SlicingCollection, slice_at_range, SliceNodeJoiner, SlicingGroup
@register_class @register_class
class SLICENODE_GetPaths(SliceNode): class SLICENODE_GetPaths(SliceNode):
@ -44,13 +44,21 @@ class SLICENODE_SliceShells(SliceNode):
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths: def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
if socket_name != "Paths": return SlicePaths() if socket_name != "Paths": return SlicePaths()
coll: SlicingCollection | None = self.resolve("Mesh") coll: SlicingCollection | SlicingGroup | None = self.resolve("Mesh")
layer_height: float = self.resolve("Layer Height") layer_height: float = self.resolve("Layer Height")
rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0) rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0)
if not coll: return SlicePaths() if not coll: return SlicePaths()
merged_object = SlicingObject.merge(coll.objects) # 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): if rotation and any(rotation):
from mathutils import Euler from mathutils import Euler
@ -127,6 +135,27 @@ class SLICENODE_PathOffset(SliceNode):
return paths.offset(np.array(offset, dtype=np.float32)) 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("SLICESOCK_Float", "Angle").value = 0.0
self.add_input("SLICESOCK_Vector", "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 @register_class
class SLICENODE_BufferShellsMultiple(SliceNode): class SLICENODE_BufferShellsMultiple(SliceNode):
bl_label = "Buffer Shells Multiple" bl_label = "Buffer Shells Multiple"

View file

@ -505,6 +505,7 @@ class SLICENODE_MT_Geometry(Menu):
layout.operator("node.add_node", text="Buffer Shells").type = "SLICENODE_BufferShells" layout.operator("node.add_node", text="Buffer Shells").type = "SLICENODE_BufferShells"
layout.operator("node.add_node", text="Buffer Shells Multiple").type = "SLICENODE_BufferShellsMultiple" layout.operator("node.add_node", text="Buffer Shells Multiple").type = "SLICENODE_BufferShellsMultiple"
layout.operator("node.add_node", text="Path Offset").type = "SLICENODE_PathOffset" layout.operator("node.add_node", text="Path Offset").type = "SLICENODE_PathOffset"
layout.operator("node.add_node", text="Rotate Path").type = "SLICENODE_RotatePath"
layout.operator("node.add_node", text="Infill").type = "SLICENODE_Infill" layout.operator("node.add_node", text="Infill").type = "SLICENODE_Infill"
layout.operator("node.add_node", text="Boolean Paths").type = "SLICENODE_JoinSlices" layout.operator("node.add_node", text="Boolean Paths").type = "SLICENODE_JoinSlices"
layout.separator() layout.separator()

View file

@ -102,6 +102,50 @@ class SlicePaths(list[np.ndarray]):
return new_paths return new_paths
def rotate(self, angle_deg: float, center: np.ndarray | None = None) -> SlicePaths:
"""Rotates the paths around Z-axis."""
if angle_deg == 0:
return self
if center is None:
center = self.get_first_layer_midpoint()
angle_rad = np.radians(angle_deg)
c, s = np.cos(angle_rad), np.sin(angle_rad)
rot_mat = np.array([
[c, -s, 0],
[s, c, 0],
[0, 0, 1]
], dtype=np.float32)
new_paths = []
for p in self:
# Shift to center, rotate, shift back
new_p = (p - center) @ rot_mat.T + center
new_paths.append(new_p)
return self.copy_with(new_paths)
def get_first_layer_midpoint(self) -> np.ndarray:
if not self:
return np.zeros(3)
# Find minimum group index
min_g = min(self.groups)
first_layer_indices = [i for i, g in enumerate(self.groups) if g == min_g]
all_pts = []
for i in first_layer_indices:
all_pts.append(self[i])
if not all_pts:
return np.zeros(3)
pts = np.concatenate(all_pts)
min_pts = np.min(pts, axis=0)
max_pts = np.max(pts, axis=0)
return (min_pts + max_pts) / 2.0
def boolean(self, other: SlicePaths, operation: str = 'UNION', connect_along_boundary: bool = False) -> SlicePaths: def boolean(self, other: SlicePaths, operation: str = 'UNION', connect_along_boundary: bool = False) -> SlicePaths:
return boolean_slice(self, other, operation, connect_along_boundary=connect_along_boundary) return boolean_slice(self, other, operation, connect_along_boundary=connect_along_boundary)
@ -840,10 +884,10 @@ class Pather(object):
gcode.append(f";AFTER_LAYER_CHANGE") gcode.append(f";AFTER_LAYER_CHANGE")
last_z = z0 last_z = z0
gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} Z{z0:.3f}") old_fr = f_rate[0] if f_rate[0] > 0 else self.feedrate
old_x, old_y, old_z = x0, y0, z0 gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} Z{z0:.3f} F{old_fr}")
old_fr = 0 old_x, old_y, old_z = x0, y0, z0
old_extras = {prefix: None for prefix in extras} old_extras = {prefix: None for prefix in extras}
for j, (p1, p2) in enumerate(zip(coords, coords[1:])): for j, (p1, p2) in enumerate(zip(coords, coords[1:])):
seg_len: float = segment_length_3d(p1, p2) seg_len: float = segment_length_3d(p1, p2)

View file

@ -57,6 +57,10 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **Extrusion Multiplier Attribute**: Mesh attribute to use for per-segment extrusion rates. - **Extrusion Multiplier Attribute**: Mesh attribute to use for per-segment extrusion rates.
- **Feedrate Attribute**: Mesh attribute to use for per-segment feedrates. - **Feedrate Attribute**: Mesh attribute to use for per-segment feedrates.
- **Extra Attributes**: Dictionary mapping mesh attribute names to custom G-code prefixes (e.g., `opening_size` -> `S`). - **Extra Attributes**: Dictionary mapping mesh attribute names to custom G-code prefixes (e.g., `opening_size` -> `S`).
- **Path Offset**: Offsets `SlicePaths` by a 3D vector.
- **Rotate Path**: Rotates `SlicePaths` around the Z-axis.
- **Angle**: Rotation angle in degrees.
- **Center**: Optional center of rotation. If not provided, it defaults to the midpoint of the first layer.
- **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height. Supports rotation for slicing at an angle. - **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height. Supports rotation for slicing at an angle.
- **Buffer Shells**: Offsets/buffers 2D `SlicePaths`. Supports rotated offsetting and multiple miter types. - **Buffer Shells**: Offsets/buffers 2D `SlicePaths`. Supports rotated offsetting and multiple miter types.
- **Infill**: Generates a cross-hatch grid or parallel lines clipped by boundary paths. - **Infill**: Generates a cross-hatch grid or parallel lines clipped by boundary paths.

View file

@ -0,0 +1,14 @@
G90
M83
; process 25mm-TRUE
; layer 1, Z = 10.000
T0
G92 E0.0000
G1 E-2.0000 F4800
; feature inner perimeter
; tool H10.000 W29.700
G1 Z15.000 F6000
G1 X755.261 Y904.113 F6000
G1 Z10.000 F6000
G1 E0.0000 F4800
G92 E0.0000