From 57a5570e140a9c8804b99f7208812b6382314544 Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Thu, 14 May 2026 21:11:40 +0300 Subject: [PATCH] up --- core/nodeslicer/nodes.py | 45 ++++++++++++++----- core/slice.py | 82 ++++++++++++++++++++++------------- doc/MODULES.md | 2 +- doc/NODES.md | 7 ++- infra/blender_mesh_capture.py | 6 --- 5 files changed, 93 insertions(+), 49 deletions(-) diff --git a/core/nodeslicer/nodes.py b/core/nodeslicer/nodes.py index fa2763d..1b0e5c9 100644 --- a/core/nodeslicer/nodes.py +++ b/core/nodeslicer/nodes.py @@ -5,7 +5,7 @@ from typing import Any, TypeAlias import bpy import numpy as np from mathutils import Euler -from bpy.props import StringProperty, FloatProperty, IntProperty, EnumProperty +from bpy.props import StringProperty, FloatProperty, IntProperty, EnumProperty, BoolProperty from ...registry import register_class from .. import slice @@ -384,6 +384,15 @@ class SLICENODE_BufferShellsMultiple(SliceNode): class SLICENODE_Infill(SliceNode): bl_label = "Infill" + infill_type: EnumProperty( + name="Infill Type", + items=[ + ('GRID', "Grid", "Cross-hatch grid infill"), + ('RECTILINEAR', "Rectilinear", "Alternating parallel lines"), + ], + default='GRID' + ) # pyright: ignore[reportInvalidTypeForm] + def init(self, context): self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Float", "Density").value = 2.0 @@ -391,6 +400,10 @@ class SLICENODE_Infill(SliceNode): 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() @@ -410,7 +423,12 @@ class SLICENODE_Infill(SliceNode): else: p = paths - res_paths, res_groups = slice.infill_slice(p, density, rotation_deg=infill_rotation) + res_paths, res_groups = slice.infill_slice( + p, + density, + rotation_deg=infill_rotation, + infill_type=self.infill_type # pyright: ignore[reportOptionalMemberAccess] + ) if any(rotation): # Re-rotate shells back @@ -426,11 +444,17 @@ class SLICENODE_Infill(SliceNode): class SLICENODE_PathsToGcode(SliceNode): bl_label = "Paths to Gcode" + use_header: BoolProperty(name="Header", default=True) # pyright: ignore[reportInvalidTypeForm] + def init(self, context): self.add_input("SLICESOCK_Paths") self.add_input("SLICESOCK_Intrinsics") self.add_output("SLICESOCK_Gcode") + def draw_buttons(self, context, layout): + assert layout + layout.prop(self, "use_header") + def evaluate_output(self, socket_name: str, **_kwargs) -> str: paths: list[np.ndarray] = resolve_input(self.inputs.get("Paths")) intrinsics: SliceIntrinsics | None = resolve_input(self.inputs.get("Intrinsics")) @@ -444,7 +468,7 @@ class SLICENODE_PathsToGcode(SliceNode): pather.feedrate = intrinsics.feedrate pather.extrusion_rate = intrinsics.extrusion_rate - gcode: str = pather.to_gcode() + gcode: str = pather.to_gcode(include_header=self.use_header) return gcode @register_class @@ -924,15 +948,12 @@ class SLICENODE_GcodeToMesh(ExecSliceNode): new_e = params.get('E', last_e) if cmd == 'G1': - is_extruding = new_e > 0 - - if is_extruding: - # Subtract offsets (scaled to Blender units) to return to original space - ox, oy = offset_x * 0.001, offset_y * 0.001 - segments.append(( - (last_x - ox, last_y - oy, last_z), - (new_x - ox, new_y - oy, new_z) - )) + # Subtract offsets (scaled to Blender units) to return to original space + ox, oy = offset_x * 0.001, offset_y * 0.001 + segments.append(( + (last_x - ox, last_y - oy, last_z), + (new_x - ox, new_y - oy, new_z) + )) last_x, last_y, last_z = new_x, new_y, new_z last_e = new_e diff --git a/core/slice.py b/core/slice.py index 017b139..ba382ce 100644 --- a/core/slice.py +++ b/core/slice.py @@ -1,6 +1,4 @@ from __future__ import annotations -from typing import Any -from numpy import ndarray import numpy as np import pyclipper @@ -324,8 +322,8 @@ def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round', return paths_list, groups_list @ndarray_lru_cache(maxsize=32) -def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float = 0.0) -> tuple[list[np.ndarray], list[int]]: - """Generates a grid infill clipped by the provided paths. Consistent across layers.""" +def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float = 0.0, infill_type: str = 'GRID') -> tuple[list[np.ndarray], list[int]]: + """Generates a grid or rectilinear infill clipped by the provided paths. Consistent across layers.""" if not slice_paths or density <= 0: return [], [] rotation = np.radians(rotation_deg) @@ -358,21 +356,19 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float rg_min_y -= density rg_max_y += density - scaled_grid_paths = [] - - # Vertical lines + vertical_lines = [] x = rg_min_x while x <= rg_max_x: - scaled_grid_paths.append([ + vertical_lines.append([ (int(x * SCALE), int(rg_min_y * SCALE)), (int(x * SCALE), int(rg_max_y * SCALE)) ]) x += density - # Horizontal lines + horizontal_lines = [] y = rg_min_y while y <= rg_max_y: - scaled_grid_paths.append([ + horizontal_lines.append([ (int(rg_min_x * SCALE), int(y * SCALE)), (int(rg_max_x * SCALE), int(y * SCALE)) ]) @@ -392,7 +388,19 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float output_paths: list[np.ndarray] = [] output_groups: list[int] = [] + # Sort groups by Z to determine layer index for alternating rectilinear infill + unique_groups = sorted(groups.keys()) + group_to_layer_index = {g: i for i, g in enumerate(unique_groups)} + for g in groups.keys(): + layer_idx = group_to_layer_index[g] + + if infill_type == 'GRID': + current_grid_paths = vertical_lines + horizontal_lines + elif infill_type == 'RECTILINEAR': + current_grid_paths = vertical_lines if layer_idx % 2 == 0 else horizontal_lines + else: + current_grid_paths = [] layer_paths = groups[g] z = layer_paths[0][0, 2] @@ -412,8 +420,8 @@ def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float scaled = (path_pts * SCALE).astype(np.int64).tolist() pc.AddPath(scaled, pyclipper.PT_CLIP, True) # pyright: ignore[reportAttributeAccessIssue] - if scaled_grid_paths: - pc.AddPaths(scaled_grid_paths, pyclipper.PT_SUBJECT, False) # pyright: ignore[reportAttributeAccessIssue] + if current_grid_paths: + pc.AddPaths(current_grid_paths, pyclipper.PT_SUBJECT, False) # pyright: ignore[reportAttributeAccessIssue] solution = pc.Execute2(pyclipper.CT_INTERSECTION, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD) # pyright: ignore[reportAttributeAccessIssue] clipped_lines = pyclipper.OpenPathsFromPolyTree(solution) # pyright: ignore[reportAttributeAccessIssue] @@ -446,7 +454,7 @@ class Pather(object): self.feedrate: float = 1200. self.extrusion_rate: float = 0.017 - def to_gcode(self) -> str: + def to_gcode(self, include_header: bool = True) -> str: if not self.paths: return "" @@ -473,18 +481,22 @@ class Pather(object): dz = p2[2] - p1[2] return (dx * dx + dy * dy + dz * dz) ** 0.5 - gcode: list[str] = [ - "; Generated from NodeSlicer", - f";TYPE:Custom", - f";FEEDRATE={self.feedrate}", - f";EXTRUSION_RATE={self.extrusion_rate}", - f";OFFSET_X={offset_x:.6f}", - f";OFFSET_Y={offset_y:.6f}", - "G21 ; set units to mm", - "G90 ; absolute XYZ positioning", - "M83 ; relative extrusion mode", - "G92 E0", - ] + gcode: list[str] = [] + + if include_header: + header: list[str] = [ + "; Generated from NodeSlicer", + f";TYPE:Custom", + f";FEEDRATE={self.feedrate}", + f";EXTRUSION_RATE={self.extrusion_rate}", + f";OFFSET_X={offset_x:.6f}", + f";OFFSET_Y={offset_y:.6f}", + "G21 ; set units to mm", + "G90 ; absolute XYZ positioning", + "M83 ; relative extrusion mode", + "G92 E0", + ] + gcode += header last_z = -1.0 max_z = 0 @@ -503,15 +515,27 @@ class Pather(object): last_z = z0 gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} Z{z0:.3f}") - gcode.append(";TYPE:External perimeter") + fr = self.feedrate + old_fr = 0 for p1, p2 in zip(coords, coords[1:]): seg_len: float = segment_length_3d(p1, p2) e_segment = seg_len * self.extrusion_rate + + old_x, old_y, old_z = p1 x, y, z = p2 - gcode.append( - f"G1 X{x:.3f} Y{y:.3f} Z{z:.3f} E{e_segment:.5f} F{self.feedrate}" - ) + + cur_gcode = "G1" + + if not old_x == x: cur_gcode += f" X{x:.3f}" + if not old_y == y: cur_gcode += f" Y{y:.3f}" + if not old_z == z: cur_gcode += f" Z{z:.3f}" + if e_segment: cur_gcode += f" E{e_segment:.5f}" + if not old_fr == fr: cur_gcode += f" F{fr:.3f}" + + gcode += [cur_gcode] + max_z = max(max_z, z) + old_fr = fr return "\n".join(gcode) \ No newline at end of file diff --git a/doc/MODULES.md b/doc/MODULES.md index d448735..e329b22 100644 --- a/doc/MODULES.md +++ b/doc/MODULES.md @@ -32,7 +32,7 @@ This file provides a mapping of modules to their primary responsibilities and ke - `segments_to_slice()`: Utility to convert raw intersection segments into closed loops using Pyclipper. - `link_segments()`: Logic for joining loose segments into continuous paths. - `buffer_slice()`: Offset utility using PyclipperOffset. Supports rotated offsetting. - - `infill_slice()`: Generates rotated grid infill patterns, ensuring layer-to-layer consistency. + - `infill_slice()`: Generates rotated grid or rectilinear infill patterns, ensuring layer-to-layer consistency. - `boolean_slice()`: Performs 2D boolean operations (Union, Intersection, etc.) on slice sets. - `geometry.py`: diff --git a/doc/NODES.md b/doc/NODES.md index 093db53..d4ec9b0 100644 --- a/doc/NODES.md +++ b/doc/NODES.md @@ -53,7 +53,11 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ - **Get Paths**: Extracts existing edges from a mesh as paths. - **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height. - **Buffer Shells**: Offsets/buffers 2D `SlicePaths`. Supports rotated offsetting and multiple miter types. -- **Infill**: Generates a cross-hatch grid of lines clipped by boundary paths. Supports rotation in degrees. +- **Infill**: Generates a cross-hatch grid or parallel lines clipped by boundary paths. + - **Infill Type**: Switch between **Grid** (cross-hatch) and **Rectilinear** (alternating parallel lines). + - **Density**: Distance between lines in units. + - **Infill Rotation**: Base rotation of the infill pattern. + - **Layer Rotation**: Rotation of the entire slice to align infill with specific planes. - **Boolean Paths**: Performs 2D boolean operations (`UNION`, `INTERSECTION`, `DIFFERENCE`, `XOR`) on multiple slice sets. ### Utility & Joiners @@ -64,6 +68,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ ### Action & I/O - **Paths to Gcode**: Converts `SlicePaths` into G-code blocks. + - **Header**: Toggles the generation of standard NodeSlicer G-code headers and units setup. - **Load Gcode**: Reads a G-code file from disk. - **Gcode to File**: Writes G-code string to a file on disk. - **Gcode to Mesh**: Decodes G-code and creates a Blender mesh ("Debug" object) for visualization. diff --git a/infra/blender_mesh_capture.py b/infra/blender_mesh_capture.py index 8b722ff..d66c9c4 100644 --- a/infra/blender_mesh_capture.py +++ b/infra/blender_mesh_capture.py @@ -287,9 +287,6 @@ class TriMesh(): self.length_tris = t self.matrix_world = m -from ..core.geometry import ndarray_lru_cache - -@ndarray_lru_cache(maxsize=32) def _cached_objects_to_tris(obj_names: tuple[str, ...], matrices: tuple[tuple[float, ...], ...], scale: float) -> NDArray[float64]: # Implementation remains largely the same but uses names to fetch objects tris_count: int = 0 @@ -361,9 +358,6 @@ def objects_to_tris(objects: list[Object], scale) -> NDArray[float64]: matrices = tuple(tuple(val for row in o.matrix_world for val in row) for o in objects) return _cached_objects_to_tris(obj_names, matrices, scale) -from functools import lru_cache - -@lru_cache(maxsize=64) def _cached_capture_mesh_edges( obj_name: str, mesh_name: str,