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_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
|
||||
class SLICENODE_GetPaths(SliceNode):
|
||||
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="Overhang Mask").type = "SLICENODE_OverhangMask"
|
||||
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.operator("node.add_node", text="Math").type = "SLICENODE_FloatMath"
|
||||
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="Meshes to List").type = "SLICENODE_MeshesToList"
|
||||
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.separator()
|
||||
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.
|
||||
- **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.
|
||||
- **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.
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -472,6 +472,64 @@ class TestNodeEvaluationsWithLists(unittest.TestCase):
|
|||
self.assertEqual(set(recovered_group.collections.keys()), {"Part1", "Part2"})
|
||||
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):
|
||||
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_JoinMesh
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue