From dbc631e3ddce46360539a9af7fd1c20dea872ec5 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Fri, 26 Jun 2026 13:39:13 +0300 Subject: [PATCH] rotate --- core/nodeslicer/nodes/path.py | 35 ++++++++++++-- core/nodeslicer/tree.py | 1 + core/slice.py | 50 ++++++++++++++++++-- doc/NODES.md | 4 ++ gcode_snippets/concrete_printer_header.gcode | 14 ++++++ 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 gcode_snippets/concrete_printer_header.gcode diff --git a/core/nodeslicer/nodes/path.py b/core/nodeslicer/nodes/path.py index 0705ced..90f39a4 100644 --- a/core/nodeslicer/nodes/path.py +++ b/core/nodeslicer/nodes/path.py @@ -4,7 +4,7 @@ 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 +from .base import SliceNode, SlicePaths, SlicingObject, SlicingCollection, slice_at_range, SliceNodeJoiner, SlicingGroup @register_class class SLICENODE_GetPaths(SliceNode): @@ -44,13 +44,21 @@ class SLICENODE_SliceShells(SliceNode): def evaluate_output(self, socket_name: str, **_kwargs) -> 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") rotation: Any = self.resolve("Rotation") or (0.0, 0.0, 0.0) 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): from mathutils import Euler @@ -127,6 +135,27 @@ class SLICENODE_PathOffset(SliceNode): 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 class SLICENODE_BufferShellsMultiple(SliceNode): bl_label = "Buffer Shells Multiple" diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index bf44c4d..5d7e39e 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -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 Multiple").type = "SLICENODE_BufferShellsMultiple" 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="Boolean Paths").type = "SLICENODE_JoinSlices" layout.separator() diff --git a/core/slice.py b/core/slice.py index 23a4ab2..0acc8ea 100644 --- a/core/slice.py +++ b/core/slice.py @@ -102,6 +102,50 @@ class SlicePaths(list[np.ndarray]): 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: 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") last_z = z0 - gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} Z{z0:.3f}") - old_x, old_y, old_z = x0, y0, z0 + old_fr = f_rate[0] if f_rate[0] > 0 else self.feedrate + 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} for j, (p1, p2) in enumerate(zip(coords, coords[1:])): seg_len: float = segment_length_3d(p1, p2) diff --git a/doc/NODES.md b/doc/NODES.md index 34c1be3..a06c072 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -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. - **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`). +- **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. - **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. diff --git a/gcode_snippets/concrete_printer_header.gcode b/gcode_snippets/concrete_printer_header.gcode new file mode 100644 index 0000000..73cdf1e --- /dev/null +++ b/gcode_snippets/concrete_printer_header.gcode @@ -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