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 bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from mathutils import Euler
|
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 ...registry import register_class
|
||||||
from .. import slice
|
from .. import slice
|
||||||
|
|
@ -384,6 +384,15 @@ class SLICENODE_BufferShellsMultiple(SliceNode):
|
||||||
class SLICENODE_Infill(SliceNode):
|
class SLICENODE_Infill(SliceNode):
|
||||||
bl_label = "Infill"
|
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):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Paths")
|
self.add_input("SLICESOCK_Paths")
|
||||||
self.add_input("SLICESOCK_Float", "Density").value = 2.0
|
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_input("SLICESOCK_VectorRotation", "Layer Rotation")
|
||||||
self.add_output("SLICESOCK_Paths")
|
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:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||||
if socket_name != "Paths": return SlicePaths()
|
if socket_name != "Paths": return SlicePaths()
|
||||||
|
|
||||||
|
|
@ -410,7 +423,12 @@ class SLICENODE_Infill(SliceNode):
|
||||||
else:
|
else:
|
||||||
p = paths
|
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):
|
if any(rotation):
|
||||||
# Re-rotate shells back
|
# Re-rotate shells back
|
||||||
|
|
@ -426,11 +444,17 @@ class SLICENODE_Infill(SliceNode):
|
||||||
class SLICENODE_PathsToGcode(SliceNode):
|
class SLICENODE_PathsToGcode(SliceNode):
|
||||||
bl_label = "Paths to Gcode"
|
bl_label = "Paths to Gcode"
|
||||||
|
|
||||||
|
use_header: BoolProperty(name="Header", default=True) # pyright: ignore[reportInvalidTypeForm]
|
||||||
|
|
||||||
def init(self, context):
|
def init(self, context):
|
||||||
self.add_input("SLICESOCK_Paths")
|
self.add_input("SLICESOCK_Paths")
|
||||||
self.add_input("SLICESOCK_Intrinsics")
|
self.add_input("SLICESOCK_Intrinsics")
|
||||||
self.add_output("SLICESOCK_Gcode")
|
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:
|
def evaluate_output(self, socket_name: str, **_kwargs) -> str:
|
||||||
paths: list[np.ndarray] = resolve_input(self.inputs.get("Paths"))
|
paths: list[np.ndarray] = resolve_input(self.inputs.get("Paths"))
|
||||||
intrinsics: SliceIntrinsics | None = resolve_input(self.inputs.get("Intrinsics"))
|
intrinsics: SliceIntrinsics | None = resolve_input(self.inputs.get("Intrinsics"))
|
||||||
|
|
@ -444,7 +468,7 @@ class SLICENODE_PathsToGcode(SliceNode):
|
||||||
pather.feedrate = intrinsics.feedrate
|
pather.feedrate = intrinsics.feedrate
|
||||||
pather.extrusion_rate = intrinsics.extrusion_rate
|
pather.extrusion_rate = intrinsics.extrusion_rate
|
||||||
|
|
||||||
gcode: str = pather.to_gcode()
|
gcode: str = pather.to_gcode(include_header=self.use_header)
|
||||||
return gcode
|
return gcode
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
|
|
@ -924,15 +948,12 @@ class SLICENODE_GcodeToMesh(ExecSliceNode):
|
||||||
new_e = params.get('E', last_e)
|
new_e = params.get('E', last_e)
|
||||||
|
|
||||||
if cmd == 'G1':
|
if cmd == 'G1':
|
||||||
is_extruding = new_e > 0
|
# Subtract offsets (scaled to Blender units) to return to original space
|
||||||
|
ox, oy = offset_x * 0.001, offset_y * 0.001
|
||||||
if is_extruding:
|
segments.append((
|
||||||
# Subtract offsets (scaled to Blender units) to return to original space
|
(last_x - ox, last_y - oy, last_z),
|
||||||
ox, oy = offset_x * 0.001, offset_y * 0.001
|
(new_x - ox, new_y - oy, new_z)
|
||||||
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_x, last_y, last_z = new_x, new_y, new_z
|
||||||
last_e = new_e
|
last_e = new_e
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Any
|
|
||||||
from numpy import ndarray
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyclipper
|
import pyclipper
|
||||||
|
|
||||||
|
|
@ -324,8 +322,8 @@ def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round',
|
||||||
return paths_list, groups_list
|
return paths_list, groups_list
|
||||||
|
|
||||||
@ndarray_lru_cache(maxsize=32)
|
@ndarray_lru_cache(maxsize=32)
|
||||||
def infill_slice(slice_paths: 'SlicePaths', density: float, rotation_deg: float = 0.0) -> tuple[list[np.ndarray], list[int]]:
|
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 infill clipped by the provided paths. Consistent across layers."""
|
"""Generates a grid or rectilinear infill clipped by the provided paths. Consistent across layers."""
|
||||||
if not slice_paths or density <= 0: return [], []
|
if not slice_paths or density <= 0: return [], []
|
||||||
|
|
||||||
rotation = np.radians(rotation_deg)
|
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_min_y -= density
|
||||||
rg_max_y += density
|
rg_max_y += density
|
||||||
|
|
||||||
scaled_grid_paths = []
|
vertical_lines = []
|
||||||
|
|
||||||
# Vertical lines
|
|
||||||
x = rg_min_x
|
x = rg_min_x
|
||||||
while x <= rg_max_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_min_y * SCALE)),
|
||||||
(int(x * SCALE), int(rg_max_y * SCALE))
|
(int(x * SCALE), int(rg_max_y * SCALE))
|
||||||
])
|
])
|
||||||
x += density
|
x += density
|
||||||
|
|
||||||
# Horizontal lines
|
horizontal_lines = []
|
||||||
y = rg_min_y
|
y = rg_min_y
|
||||||
while y <= rg_max_y:
|
while y <= rg_max_y:
|
||||||
scaled_grid_paths.append([
|
horizontal_lines.append([
|
||||||
(int(rg_min_x * SCALE), int(y * SCALE)),
|
(int(rg_min_x * SCALE), int(y * SCALE)),
|
||||||
(int(rg_max_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_paths: list[np.ndarray] = []
|
||||||
output_groups: list[int] = []
|
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():
|
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]
|
layer_paths = groups[g]
|
||||||
z = layer_paths[0][0, 2]
|
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()
|
scaled = (path_pts * SCALE).astype(np.int64).tolist()
|
||||||
pc.AddPath(scaled, pyclipper.PT_CLIP, True) # pyright: ignore[reportAttributeAccessIssue]
|
pc.AddPath(scaled, pyclipper.PT_CLIP, True) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
||||||
if scaled_grid_paths:
|
if current_grid_paths:
|
||||||
pc.AddPaths(scaled_grid_paths, pyclipper.PT_SUBJECT, False) # pyright: ignore[reportAttributeAccessIssue]
|
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]
|
solution = pc.Execute2(pyclipper.CT_INTERSECTION, pyclipper.PFT_EVENODD, pyclipper.PFT_EVENODD) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
clipped_lines = pyclipper.OpenPathsFromPolyTree(solution) # pyright: ignore[reportAttributeAccessIssue]
|
clipped_lines = pyclipper.OpenPathsFromPolyTree(solution) # pyright: ignore[reportAttributeAccessIssue]
|
||||||
|
|
@ -446,7 +454,7 @@ class Pather(object):
|
||||||
self.feedrate: float = 1200.
|
self.feedrate: float = 1200.
|
||||||
self.extrusion_rate: float = 0.017
|
self.extrusion_rate: float = 0.017
|
||||||
|
|
||||||
def to_gcode(self) -> str:
|
def to_gcode(self, include_header: bool = True) -> str:
|
||||||
if not self.paths:
|
if not self.paths:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
@ -473,18 +481,22 @@ class Pather(object):
|
||||||
dz = p2[2] - p1[2]
|
dz = p2[2] - p1[2]
|
||||||
return (dx * dx + dy * dy + dz * dz) ** 0.5
|
return (dx * dx + dy * dy + dz * dz) ** 0.5
|
||||||
|
|
||||||
gcode: list[str] = [
|
gcode: list[str] = []
|
||||||
"; Generated from NodeSlicer",
|
|
||||||
f";TYPE:Custom",
|
if include_header:
|
||||||
f";FEEDRATE={self.feedrate}",
|
header: list[str] = [
|
||||||
f";EXTRUSION_RATE={self.extrusion_rate}",
|
"; Generated from NodeSlicer",
|
||||||
f";OFFSET_X={offset_x:.6f}",
|
f";TYPE:Custom",
|
||||||
f";OFFSET_Y={offset_y:.6f}",
|
f";FEEDRATE={self.feedrate}",
|
||||||
"G21 ; set units to mm",
|
f";EXTRUSION_RATE={self.extrusion_rate}",
|
||||||
"G90 ; absolute XYZ positioning",
|
f";OFFSET_X={offset_x:.6f}",
|
||||||
"M83 ; relative extrusion mode",
|
f";OFFSET_Y={offset_y:.6f}",
|
||||||
"G92 E0",
|
"G21 ; set units to mm",
|
||||||
]
|
"G90 ; absolute XYZ positioning",
|
||||||
|
"M83 ; relative extrusion mode",
|
||||||
|
"G92 E0",
|
||||||
|
]
|
||||||
|
gcode += header
|
||||||
|
|
||||||
last_z = -1.0
|
last_z = -1.0
|
||||||
max_z = 0
|
max_z = 0
|
||||||
|
|
@ -503,15 +515,27 @@ class Pather(object):
|
||||||
last_z = z0
|
last_z = z0
|
||||||
|
|
||||||
gcode.append(f"G0 X{x0:.3f} Y{y0:.3f} Z{z0:.3f}")
|
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:]):
|
for p1, p2 in zip(coords, coords[1:]):
|
||||||
seg_len: float = segment_length_3d(p1, p2)
|
seg_len: float = segment_length_3d(p1, p2)
|
||||||
e_segment = seg_len * self.extrusion_rate
|
e_segment = seg_len * self.extrusion_rate
|
||||||
|
|
||||||
|
old_x, old_y, old_z = p1
|
||||||
x, y, z = p2
|
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)
|
max_z = max(max_z, z)
|
||||||
|
old_fr = fr
|
||||||
|
|
||||||
return "\n".join(gcode)
|
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.
|
- `segments_to_slice()`: Utility to convert raw intersection segments into closed loops using Pyclipper.
|
||||||
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
- `link_segments()`: Logic for joining loose segments into continuous paths.
|
||||||
- `buffer_slice()`: Offset utility using PyclipperOffset. Supports rotated offsetting.
|
- `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.
|
- `boolean_slice()`: Performs 2D boolean operations (Union, Intersection, etc.) on slice sets.
|
||||||
|
|
||||||
- `geometry.py`:
|
- `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.
|
- **Get Paths**: Extracts existing edges from a mesh as paths.
|
||||||
- **Slice Shells**: Slices a `SlicingObject` into 2D `SlicePaths` at a given layer height.
|
- **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.
|
- **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.
|
- **Boolean Paths**: Performs 2D boolean operations (`UNION`, `INTERSECTION`, `DIFFERENCE`, `XOR`) on multiple slice sets.
|
||||||
|
|
||||||
### Utility & Joiners
|
### Utility & Joiners
|
||||||
|
|
@ -64,6 +68,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
|
|
||||||
### Action & I/O
|
### Action & I/O
|
||||||
- **Paths to Gcode**: Converts `SlicePaths` into G-code blocks.
|
- **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.
|
- **Load Gcode**: Reads a G-code file from disk.
|
||||||
- **Gcode to File**: Writes G-code string to a file on 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.
|
- **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.length_tris = t
|
||||||
self.matrix_world = m
|
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]:
|
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
|
# Implementation remains largely the same but uses names to fetch objects
|
||||||
tris_count: int = 0
|
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)
|
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)
|
return _cached_objects_to_tris(obj_names, matrices, scale)
|
||||||
|
|
||||||
from functools import lru_cache
|
|
||||||
|
|
||||||
@lru_cache(maxsize=64)
|
|
||||||
def _cached_capture_mesh_edges(
|
def _cached_capture_mesh_edges(
|
||||||
obj_name: str,
|
obj_name: str,
|
||||||
mesh_name: str,
|
mesh_name: str,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue