This commit is contained in:
Nicolas Predella 2026-05-14 21:11:40 +03:00
parent ce6c88e994
commit 57a5570e14
5 changed files with 93 additions and 49 deletions

View file

@ -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

View file

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

View file

@ -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`:

View file

@ -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.

View file

@ -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,