flatten paths
This commit is contained in:
parent
31a86fbb73
commit
1604e6da23
4 changed files with 112 additions and 0 deletions
|
|
@ -14,6 +14,54 @@ class SLICENODE_PathsToList(SliceNodeListJoiner):
|
||||||
socket_type = "SLICESOCK_Paths"
|
socket_type = "SLICESOCK_Paths"
|
||||||
socket_label = "Paths"
|
socket_label = "Paths"
|
||||||
|
|
||||||
|
@register_class
|
||||||
|
class SLICENODE_FlattenPathList(SliceNode):
|
||||||
|
bl_label = "Flatten Path List"
|
||||||
|
always_list_inputs = ["Paths"]
|
||||||
|
|
||||||
|
mode: str = NfEnumProperty(
|
||||||
|
name="Mode",
|
||||||
|
items=[
|
||||||
|
('ZIP', "Zip", "Interleave paths layer by layer across all list items"),
|
||||||
|
('EXTEND', "Extend", "Append paths sequentially from all list items"),
|
||||||
|
],
|
||||||
|
default='ZIP',
|
||||||
|
update=notify_tree_changed,
|
||||||
|
)
|
||||||
|
|
||||||
|
def init(self, context):
|
||||||
|
self.add_input("SLICESOCK_Paths", "Paths", display_shape='LIST')
|
||||||
|
self.add_output("SLICESOCK_Paths", "Paths")
|
||||||
|
|
||||||
|
def draw_buttons(self, context, layout):
|
||||||
|
assert layout
|
||||||
|
layout.prop(self, "mode", text="")
|
||||||
|
|
||||||
|
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicePaths:
|
||||||
|
val = self.resolve("Paths")
|
||||||
|
if val is None:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
if not is_list_data(val):
|
||||||
|
if isinstance(val, SlicePaths) or getattr(val, "is_slice_paths", False):
|
||||||
|
return val
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
raw_items = flatten_list(val)
|
||||||
|
paths_list: list[SlicePaths] = [
|
||||||
|
item for item in raw_items
|
||||||
|
if isinstance(item, SlicePaths) or getattr(item, "is_slice_paths", False)
|
||||||
|
]
|
||||||
|
|
||||||
|
if not paths_list:
|
||||||
|
return SlicePaths()
|
||||||
|
|
||||||
|
mode = str(getattr(self, "mode", "ZIP")).upper()
|
||||||
|
if mode == "EXTEND":
|
||||||
|
return SlicePaths.extend_paths(paths_list)
|
||||||
|
else:
|
||||||
|
return SlicePaths.zip_paths(paths_list)
|
||||||
|
|
||||||
@register_class
|
@register_class
|
||||||
class SLICENODE_GetPaths(SliceNode):
|
class SLICENODE_GetPaths(SliceNode):
|
||||||
bl_label = "Get Paths"
|
bl_label = "Get Paths"
|
||||||
|
|
|
||||||
|
|
@ -826,6 +826,7 @@ class SLICENODE_MT_Geometry(Menu):
|
||||||
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="Overhang Mask").type = "SLICENODE_OverhangMask"
|
layout.operator("node.add_node", text="Overhang Mask").type = "SLICENODE_OverhangMask"
|
||||||
layout.operator("node.add_node", text="Path Operations").type = "SLICENODE_PathOperations"
|
layout.operator("node.add_node", text="Path Operations").type = "SLICENODE_PathOperations"
|
||||||
|
layout.operator("node.add_node", text="Flatten Path List").type = "SLICENODE_FlattenPathList"
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator("node.add_node", text="Math").type = "SLICENODE_FloatMath"
|
layout.operator("node.add_node", text="Math").type = "SLICENODE_FloatMath"
|
||||||
layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath"
|
layout.operator("node.add_node", text="Integer Math").type = "SLICENODE_IntMath"
|
||||||
|
|
@ -869,6 +870,7 @@ class SLICENODE_MT_Joiners(Menu):
|
||||||
layout.operator("node.add_node", text="Objects to List").type = "SLICENODE_ObjectsToList"
|
layout.operator("node.add_node", text="Objects to List").type = "SLICENODE_ObjectsToList"
|
||||||
layout.operator("node.add_node", text="Meshes to List").type = "SLICENODE_MeshesToList"
|
layout.operator("node.add_node", text="Meshes to List").type = "SLICENODE_MeshesToList"
|
||||||
layout.operator("node.add_node", text="Paths to List").type = "SLICENODE_PathsToList"
|
layout.operator("node.add_node", text="Paths to List").type = "SLICENODE_PathsToList"
|
||||||
|
layout.operator("node.add_node", text="Flatten Path List").type = "SLICENODE_FlattenPathList"
|
||||||
layout.operator("node.add_node", text="Gcodes to List").type = "SLICENODE_GcodesToList"
|
layout.operator("node.add_node", text="Gcodes to List").type = "SLICENODE_GcodesToList"
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator("node.add_node", text="Join Mesh Parts").type = "SLICENODE_JoinMesh"
|
layout.operator("node.add_node", text="Join Mesh Parts").type = "SLICENODE_JoinMesh"
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,10 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
|
||||||
- **Objects to List**: Multi-input list joiner that bundles objects into a `list[bpy.types.Object]` with LIST socket shape. Flattens incoming lists.
|
- **Objects to List**: Multi-input list joiner that bundles objects into a `list[bpy.types.Object]` with LIST socket shape. Flattens incoming lists.
|
||||||
- **Meshes to List**: Multi-input list joiner that bundles meshes into a `list[SlicingCollection]` with LIST socket shape. Flattens incoming lists.
|
- **Meshes to List**: Multi-input list joiner that bundles meshes into a `list[SlicingCollection]` with LIST socket shape. Flattens incoming lists.
|
||||||
- **Paths to List**: Multi-input list joiner that bundles toolpaths into a `list[SlicePaths]` with LIST socket shape. Flattens incoming lists.
|
- **Paths to List**: Multi-input list joiner that bundles toolpaths into a `list[SlicePaths]` with LIST socket shape. Flattens incoming lists.
|
||||||
|
- **Flatten Path List**: Flattens a `list[SlicePaths]` into a single `SlicePaths`.
|
||||||
|
- **Mode**: Choose between **Zip** (interleaves paths layer by layer across all list items) and **Extend** (appends paths sequentially from all list items).
|
||||||
|
- **Paths**: Input socket accepting a list of toolpaths (`display_shape='LIST'`).
|
||||||
|
- **Paths**: Output socket delivering the unified single toolpath.
|
||||||
- **Gcodes to List**: Multi-input list joiner that bundles G-code strings into a `list[str]` with LIST socket shape. Flattens incoming lists.
|
- **Gcodes to List**: Multi-input list joiner that bundles G-code strings into a `list[str]` with LIST socket shape. Flattens incoming lists.
|
||||||
- **Group to List**: Decomposes a `SlicingGroup` into a `list[SlicingCollection]`. Output socket `Collections` displays LIST socket shape.
|
- **Group to List**: Decomposes a `SlicingGroup` into a `list[SlicingCollection]`. Output socket `Collections` displays LIST socket shape.
|
||||||
- **List to Group**: Converts a `list[SlicingCollection]` into a single `SlicingGroup`. Input socket `Collections` has LIST socket shape, output socket `Group` outputs a single group.
|
- **List to Group**: Converts a `list[SlicingCollection]` into a single `SlicingGroup`. Input socket `Collections` has LIST socket shape, output socket `Group` outputs a single group.
|
||||||
|
|
|
||||||
|
|
@ -472,6 +472,64 @@ class TestNodeEvaluationsWithLists(unittest.TestCase):
|
||||||
self.assertEqual(set(recovered_group.collections.keys()), {"Part1", "Part2"})
|
self.assertEqual(set(recovered_group.collections.keys()), {"Part1", "Part2"})
|
||||||
self.assertEqual(recovered_group.transforms, orig_group.transforms)
|
self.assertEqual(recovered_group.transforms, orig_group.transforms)
|
||||||
|
|
||||||
|
def test_flatten_path_list(self):
|
||||||
|
from NodeSlicer.core.nodeslicer.nodes.path import SLICENODE_FlattenPathList
|
||||||
|
|
||||||
|
node = SLICENODE_FlattenPathList()
|
||||||
|
self.assertEqual(node.always_list_inputs, ["Paths"])
|
||||||
|
|
||||||
|
# Construct sp1: layer 0 (Z=0.2), layer 1 (Z=0.4)
|
||||||
|
sp1 = SlicePaths([
|
||||||
|
np.array([[0, 0, 0.2], [10, 0, 0.2]], dtype=np.float64),
|
||||||
|
np.array([[0, 0, 0.4], [10, 0, 0.4]], dtype=np.float64),
|
||||||
|
], groups=[0, 1])
|
||||||
|
|
||||||
|
# Construct sp2: layer 0 (Z=0.2), layer 1 (Z=0.4)
|
||||||
|
sp2 = SlicePaths([
|
||||||
|
np.array([[20, 0, 0.2], [30, 0, 0.2]], dtype=np.float64),
|
||||||
|
np.array([[20, 0, 0.4], [30, 0, 0.4]], dtype=np.float64),
|
||||||
|
], groups=[0, 1])
|
||||||
|
|
||||||
|
# 1. Mode ZIP: layer-by-layer interleaving
|
||||||
|
node.mode = 'ZIP'
|
||||||
|
node.resolve = lambda label: [sp1, sp2]
|
||||||
|
res_zip = node.evaluate_output("Paths")
|
||||||
|
self.assertIsInstance(res_zip, SlicePaths)
|
||||||
|
self.assertEqual(len(res_zip), 4)
|
||||||
|
self.assertEqual(list(res_zip.groups), [0, 0, 1, 1])
|
||||||
|
np.testing.assert_allclose(res_zip[0], sp1[0])
|
||||||
|
np.testing.assert_allclose(res_zip[1], sp2[0])
|
||||||
|
np.testing.assert_allclose(res_zip[2], sp1[1])
|
||||||
|
np.testing.assert_allclose(res_zip[3], sp2[1])
|
||||||
|
|
||||||
|
# 2. Mode EXTEND: sequential appending
|
||||||
|
node.mode = 'EXTEND'
|
||||||
|
node.resolve = lambda label: [sp1, sp2]
|
||||||
|
res_ext = node.evaluate_output("Paths")
|
||||||
|
self.assertIsInstance(res_ext, SlicePaths)
|
||||||
|
self.assertEqual(len(res_ext), 4)
|
||||||
|
self.assertEqual(list(res_ext.groups), [0, 1, 0, 1])
|
||||||
|
np.testing.assert_allclose(res_ext[0], sp1[0])
|
||||||
|
np.testing.assert_allclose(res_ext[1], sp1[1])
|
||||||
|
np.testing.assert_allclose(res_ext[2], sp2[0])
|
||||||
|
np.testing.assert_allclose(res_ext[3], sp2[1])
|
||||||
|
|
||||||
|
# 3. Nested list input flattens properly
|
||||||
|
node.resolve = lambda label: [[sp1], [sp2]]
|
||||||
|
res_nested = node.evaluate_output("Paths")
|
||||||
|
self.assertEqual(len(res_nested), 4)
|
||||||
|
|
||||||
|
# 4. Single SlicePaths input returns the object intact
|
||||||
|
node.resolve = lambda label: sp1
|
||||||
|
res_single = node.evaluate_output("Paths")
|
||||||
|
self.assertEqual(res_single, sp1)
|
||||||
|
|
||||||
|
# 5. None or empty list input returns empty SlicePaths
|
||||||
|
node.resolve = lambda label: None
|
||||||
|
self.assertEqual(len(node.evaluate_output("Paths")), 0)
|
||||||
|
node.resolve = lambda label: []
|
||||||
|
self.assertEqual(len(node.evaluate_output("Paths")), 0)
|
||||||
|
|
||||||
def test_join_mesh_flattens_lists(self):
|
def test_join_mesh_flattens_lists(self):
|
||||||
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_JoinMesh
|
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_JoinMesh
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue