get infill mask
This commit is contained in:
parent
6f5e74523a
commit
76cf856155
5 changed files with 117 additions and 0 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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]]:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue