up
This commit is contained in:
parent
ce6c88e994
commit
57a5570e14
5 changed files with 93 additions and 49 deletions
|
|
@ -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,9 +948,6 @@ 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((
|
||||
|
|
|
|||
|
|
@ -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,7 +481,10 @@ class Pather(object):
|
|||
dz = p2[2] - p1[2]
|
||||
return (dx * dx + dy * dy + dz * dz) ** 0.5
|
||||
|
||||
gcode: list[str] = [
|
||||
gcode: list[str] = []
|
||||
|
||||
if include_header:
|
||||
header: list[str] = [
|
||||
"; Generated from NodeSlicer",
|
||||
f";TYPE:Custom",
|
||||
f";FEEDRATE={self.feedrate}",
|
||||
|
|
@ -485,6 +496,7 @@ class Pather(object):
|
|||
"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)
|
||||
|
|
@ -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`:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue