get infill mask

This commit is contained in:
Nicolas Predella 2026-07-02 17:58:55 +03:00
parent 6f5e74523a
commit 76cf856155
5 changed files with 117 additions and 0 deletions

View file

@ -363,6 +363,26 @@ class SLICENODE_Infill(SliceNode):
layer_rotation=rotation layer_rotation=rotation
) )
@register_class
class SLICENODE_GetInfillMask(SliceNode):
bl_label = "Get Infill Mask"
def init(self, context):
self.add_input("SLICESOCK_Paths")
self.add_output("SLICESOCK_Paths")
def draw_buttons(self, context, layout):
pass
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
if socket_name != "Paths": return SlicePaths()
paths: SlicePaths = self.resolve("Paths")
if not paths: return SlicePaths()
return paths.get_infill_mask()
@register_class @register_class
class SLICENODE_JoinSlices(SliceNodeJoiner): class SLICENODE_JoinSlices(SliceNodeJoiner):
bl_label = "Boolean Paths" bl_label = "Boolean Paths"

View file

@ -512,6 +512,7 @@ class SLICENODE_MT_Geometry(Menu):
layout.operator("node.add_node", text="Rotate Path").type = "SLICENODE_RotatePath" layout.operator("node.add_node", text="Rotate Path").type = "SLICENODE_RotatePath"
layout.operator("node.add_node", text="Simplify Paths").type = "SLICENODE_SimplifyPaths" layout.operator("node.add_node", text="Simplify Paths").type = "SLICENODE_SimplifyPaths"
layout.operator("node.add_node", text="Infill").type = "SLICENODE_Infill" layout.operator("node.add_node", text="Infill").type = "SLICENODE_Infill"
layout.operator("node.add_node", text="Get Infill Mask").type = "SLICENODE_GetInfillMask"
layout.operator("node.add_node", text="Boolean Paths").type = "SLICENODE_JoinSlices" layout.operator("node.add_node", text="Boolean Paths").type = "SLICENODE_JoinSlices"
layout.separator() layout.separator()
layout.operator("node.add_node", text="Math").type = "SLICENODE_FloatMath" layout.operator("node.add_node", text="Math").type = "SLICENODE_FloatMath"

View file

@ -121,6 +121,11 @@ class SlicePaths(list[np.ndarray]):
return new_paths return new_paths
def get_infill_mask(self) -> SlicePaths:
"""Computes the intersection of all paths in each group using shapely."""
res_paths, res_groups = get_infill_mask_slice(self)
return self.copy_with(res_paths, res_groups)
def infill(self, density: float, rotation_deg: float = 0.0, infill_type: str = 'GRID', layer_rotation: Any = None) -> SlicePaths: def infill(self, density: float, rotation_deg: float = 0.0, infill_type: str = 'GRID', layer_rotation: Any = None) -> SlicePaths:
"""Generates infill for the paths.""" """Generates infill for the paths."""
p = self p = self
@ -836,6 +841,60 @@ def offset_curve_slice(
return paths_list, groups_list return paths_list, groups_list
@ndarray_lru_cache(maxsize=64)
def get_infill_mask_slice(slice_paths: list[np.ndarray]) -> tuple[list[np.ndarray], list[int]]:
if not slice_paths:
return [], []
from shapely.geometry import Polygon
# Group paths by their Z height
groups: dict[int, list[np.ndarray]] = {}
for p in slice_paths:
z = p[0, 2]
z_key = int(round(z * 1000))
groups.setdefault(z_key, []).append(p)
paths_list: list[np.ndarray] = []
groups_list: list[int] = []
group_id = 0
for z_key in sorted(groups.keys()):
layer_paths = groups[z_key]
if not layer_paths:
continue
z = layer_paths[0][0, 2]
# Convert each closed path to a Shapely Polygon
polygons = []
for p in layer_paths:
if len(p) < 3:
continue
pts_2d = p[:, :2]
poly = Polygon(pts_2d)
if poly.is_valid and not poly.is_empty:
polygons.append(poly)
if not polygons:
continue
# Compute the intersection of all polygons in the layer
intersection_poly = polygons[0]
for poly in polygons[1:]:
intersection_poly = intersection_poly.intersection(poly)
# Convert resulting polygon back to 3D paths
result_paths = geometry_to_paths(intersection_poly, z)
for path in result_paths:
paths_list.append(path)
groups_list.append(group_id)
group_id += 1
return paths_list, groups_list
@ndarray_lru_cache(maxsize=64) @ndarray_lru_cache(maxsize=64)
def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round', tolerance=.01) -> tuple[list[np.ndarray], list[int]]: def buffer_slice(slice_paths: list[np.ndarray], distance: float, miter='round', tolerance=.01) -> tuple[list[np.ndarray], list[int]]:

View file

@ -86,6 +86,7 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **Density**: Distance between lines in units. - **Density**: Distance between lines in units.
- **Infill Rotation**: Base rotation of the infill pattern. - **Infill Rotation**: Base rotation of the infill pattern.
- **Layer Rotation**: Rotation of the entire slice to align infill with specific planes. - **Layer Rotation**: Rotation of the entire slice to align infill with specific planes.
- **Get Infill Mask**: Computes the intersection of all closed paths in each group (layer) using Shapely.
- **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.
- **Arrange Slicing Group**: Arranges the slicing collections in a slicing group on the build plate using the `libnest2d` bin-packing library. It nests their 2D bounding boxes to prevent collisions. - **Arrange Slicing Group**: Arranges the slicing collections in a slicing group on the build plate using the `libnest2d` bin-packing library. It nests their 2D bounding boxes to prevent collisions.
- **Method**: Switch between **NFP** (No-Fit Polygon placer + First-Fit selection), **Bottom-Left** (Bottom-Left placer + First-Fit selection), and **DJD Heuristic** (No-Fit Polygon placer + Dyckhoff's heuristic). - **Method**: Switch between **NFP** (No-Fit Polygon placer + First-Fit selection), **Bottom-Left** (Bottom-Left placer + First-Fit selection), and **DJD Heuristic** (No-Fit Polygon placer + Dyckhoff's heuristic).

View file

@ -208,5 +208,41 @@ class TestSlice(unittest.TestCase):
# Check Z coordinate is preserved # Check Z coordinate is preserved
self.assertTrue(np.allclose(offset_path[:, 2], 5.0)) self.assertTrue(np.allclose(offset_path[:, 2], 5.0))
def test_get_infill_mask(self):
# Two overlapping closed squares on the same layer Z=3.0
# Square 1: (0,0) to (10,10)
sq1 = np.array([
[0.0, 0.0, 3.0],
[0.0, 10.0, 3.0],
[10.0, 10.0, 3.0],
[10.0, 0.0, 3.0],
[0.0, 0.0, 3.0]
])
# Square 2: (5,5) to (15,15) - overlaps from (5,5) to (10,10)
sq2 = np.array([
[5.0, 5.0, 3.0],
[5.0, 15.0, 3.0],
[15.0, 15.0, 3.0],
[15.0, 5.0, 3.0],
[5.0, 5.0, 3.0]
])
from NodeSlicer.core.slice import SlicePaths
# Both in group 0
slice_paths = SlicePaths([sq1, sq2], groups=[0, 0])
mask = slice_paths.get_infill_mask()
self.assertIsInstance(mask, SlicePaths)
self.assertEqual(len(mask), 1) # Intersection of two squares should be one square (5,5) to (10,10)
mask_path = mask[0]
self.assertTrue(np.allclose(mask_path[0], mask_path[-1]))
# Area of intersection should be 5 * 5 = 25
xs, ys = mask_path[:, 0], mask_path[:, 1]
area = 0.5 * np.abs(np.dot(xs, np.roll(ys, 1)) - np.dot(ys, np.roll(xs, 1)))
self.assertAlmostEqual(area, 25.0, places=3)
self.assertTrue(np.allclose(mask_path[:, 2], 3.0))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()