group-list

This commit is contained in:
Nicolas Predella 2026-10-14 17:48:12 +03:00
parent 6520e2b6f9
commit 31a86fbb73
4 changed files with 194 additions and 0 deletions

View file

@ -100,6 +100,99 @@ class SLICENODE_JoinMeshGroups(SliceNodeJoiner):
return group
@register_class
class SLICENODE_GroupToList(SliceNode):
bl_label = "Group to List"
always_list_outputs = ["Collections"]
def init(self, context):
self.add_input("SLICESOCK_Mesh", "Group")
self.add_output("SLICESOCK_Mesh", "Collections", display_shape='LIST')
def evaluate_output(self, socket_name: str, **_kwargs) -> list[SlicingCollection]:
val = self.resolve("Group")
if val is None:
val = self.resolve("Collections")
if val is None and hasattr(self, "inputs") and len(self.inputs) > 0:
val = self.resolve(self.inputs[0].name)
if val is None:
return []
if isinstance(val, SlicingGroup):
return list(val.collections.values())
if isinstance(val, (list, tuple)):
res: list[SlicingCollection] = []
for item in val:
if isinstance(item, SlicingGroup):
res.extend(item.collections.values())
elif isinstance(item, SlicingCollection):
res.append(item)
elif isinstance(item, SlicingObject):
res.append(SlicingCollection([item], getattr(item, "name", "Collection")))
return res
if isinstance(val, SlicingCollection):
return [val]
if isinstance(val, SlicingObject):
return [SlicingCollection([val], getattr(val, "name", "Collection"))]
return []
@register_class
class SLICENODE_ListToGroup(SliceNode):
bl_label = "List to Group"
always_list_inputs = ["Collections"]
def init(self, context):
self.add_input("SLICESOCK_Mesh", "Collections", display_shape='LIST')
self.add_output("SLICESOCK_Mesh", "Group")
def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingGroup:
val = self.resolve("Collections")
if val is None:
val = self.resolve("Group")
if val is None and hasattr(self, "inputs") and len(self.inputs) > 0:
val = self.resolve(self.inputs[0].name)
if val is None:
return SlicingGroup([])
if isinstance(val, SlicingGroup):
return val
items = val if isinstance(val, (list, tuple)) else [val]
group: SlicingGroup = SlicingGroup([])
for idx, item in enumerate(items):
if not item:
continue
if isinstance(item, SlicingGroup):
if hasattr(item, "transforms") and item.transforms and not group.transforms:
group.transforms = list(item.transforms)
for k, sc in item.collections.items():
name = k
if name in group.collections:
name = f"{name}_{idx}"
group.collections[name] = sc
elif isinstance(item, SlicingCollection):
if hasattr(item, "transforms") and item.transforms and not group.transforms:
group.transforms = list(item.transforms)
name = item.name or f"collection_{idx}"
if name in group.collections:
name = f"{name}_{idx}"
group.collections[name] = item
elif isinstance(item, SlicingObject):
if hasattr(item, "transforms") and item.transforms and not group.transforms:
group.transforms = list(item.transforms)
name = getattr(item, "name", f"object_{idx}")
if name in group.collections:
name = f"{name}_{idx}"
sc = SlicingCollection([item], name)
group.collections[name] = sc
return group
@register_class
class SLICENODE_SeparateMesh(SliceNode):
bl_label = "Separate Mesh"

View file

@ -808,6 +808,8 @@ class SLICENODE_MT_Geometry(Menu):
layout.operator("node.add_node", text="Separate Mesh").type = "SLICENODE_SeparateMesh"
layout.operator("node.add_node", text="Separate Mesh by Range").type = "SLICENODE_SeparateMeshByRange"
layout.operator("node.add_node", text="Arrange Slicing Group").type = "SLICENODE_ArrangeMesh"
layout.operator("node.add_node", text="Group to List").type = "SLICENODE_GroupToList"
layout.operator("node.add_node", text="List to Group").type = "SLICENODE_ListToGroup"
layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths"
layout.separator()
layout.operator("node.add_node", text="Layers").type = "SLICENODE_Layers"

View file

@ -196,6 +196,8 @@ All nodes are found in the **Add (Shift+A)** menu under the **SliceNodes** categ
- **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.
- **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.
- **Join Mesh Parts**: Merges multiple `SlicingObject` instances into one `SlicingCollection`. Flattens incoming lists.
- **Join Mesh Groups**: Combines multiple `SlicingCollection` instances into a `SlicingGroup`. Flattens incoming lists.
- **Path Operations**: Multi-input joiner performing Zip, Extend, Maximum coverage, Minimum coverage, Intersection, Subtract, and XOR operations. The first input can be a list, in which case the operation runs per-element against the same secondary inputs.

View file

@ -375,6 +375,103 @@ class TestNodeEvaluationsWithLists(unittest.TestCase):
g_node.resolve_multi_input = lambda label: [["G1 X1", "G1 X2"], "G1 X3", ["G1 X4", "G1 X5"]]
self.assertEqual(g_node.evaluate_output("Gcode"), ["G1 X1", "G1 X2", "G1 X3", "G1 X4", "G1 X5"])
def test_group_to_list(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_GroupToList
sc1 = SlicingCollection([], "PartA")
sc2 = SlicingCollection([], "PartB")
group = SlicingGroup([])
group.collections = {"PartA": sc1, "PartB": sc2}
node = SLICENODE_GroupToList()
self.assertEqual(node.always_list_outputs, ["Collections"])
# 1. Normal conversion: SlicingGroup -> list[SlicingCollection]
node.resolve = lambda label: group
res = node.evaluate_output("Collections")
self.assertIsInstance(res, list)
self.assertEqual(len(res), 2)
self.assertEqual(res, [sc1, sc2])
# 2. None input -> empty list
node.resolve = lambda label: None
self.assertEqual(node.evaluate_output("Collections"), [])
# 3. Single collection passed directly -> [sc1]
node.resolve = lambda label: sc1
self.assertEqual(node.evaluate_output("Collections"), [sc1])
# 4. List of groups -> flattened list of collections
group2 = SlicingGroup([])
sc3 = SlicingCollection([], "PartC")
group2.collections = {"PartC": sc3}
node.resolve = lambda label: [group, group2]
res_multi = node.evaluate_output("Collections")
self.assertEqual(res_multi, [sc1, sc2, sc3])
def test_list_to_group(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_ListToGroup
sc1 = SlicingCollection([], "PartA")
sc2 = SlicingCollection([], "PartB")
node = SLICENODE_ListToGroup()
self.assertEqual(node.always_list_inputs, ["Collections"])
# 1. Normal conversion: list[SlicingCollection] -> SlicingGroup
node.resolve = lambda label: [sc1, sc2]
res = node.evaluate_output("Group")
self.assertIsInstance(res, SlicingGroup)
self.assertEqual(len(res.collections), 2)
self.assertIs(res.collections.get("PartA"), sc1)
self.assertIs(res.collections.get("PartB"), sc2)
# 2. None input -> empty SlicingGroup
node.resolve = lambda label: None
res_empty = node.evaluate_output("Group")
self.assertIsInstance(res_empty, SlicingGroup)
self.assertEqual(len(res_empty.collections), 0)
# 3. Single collection -> SlicingGroup with 1 collection
node.resolve = lambda label: sc1
res_single = node.evaluate_output("Group")
self.assertIsInstance(res_single, SlicingGroup)
self.assertEqual(len(res_single.collections), 1)
self.assertIs(res_single.collections.get("PartA"), sc1)
# 4. Duplicate collection names are handled without overwriting
dup1 = SlicingCollection([], "Part")
dup2 = SlicingCollection([], "Part")
node.resolve = lambda label: [dup1, dup2]
res_dup = node.evaluate_output("Group")
self.assertEqual(len(res_dup.collections), 2)
self.assertIn("Part", res_dup.collections)
self.assertIn("Part_1", res_dup.collections)
def test_group_to_list_and_list_to_group_roundtrip(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_GroupToList, SLICENODE_ListToGroup
sc1 = SlicingCollection([], "Part1")
sc1.transforms = [{"type": "INIT", "translation": (1.0, 2.0, 3.0), "rotation": (0.0, 0.0, 0.0), "scale": (1.0, 1.0, 1.0)}]
sc2 = SlicingCollection([], "Part2")
orig_group = SlicingGroup([])
orig_group.collections = {"Part1": sc1, "Part2": sc2}
orig_group.transforms = list(sc1.transforms)
g2l = SLICENODE_GroupToList()
g2l.resolve = lambda label: orig_group
colls_list = g2l.evaluate_output("Collections")
self.assertIsInstance(colls_list, list)
self.assertEqual(len(colls_list), 2)
l2g = SLICENODE_ListToGroup()
l2g.resolve = lambda label: colls_list
recovered_group = l2g.evaluate_output("Group")
self.assertIsInstance(recovered_group, SlicingGroup)
self.assertEqual(len(recovered_group.collections), 2)
self.assertEqual(set(recovered_group.collections.keys()), {"Part1", "Part2"})
self.assertEqual(recovered_group.transforms, orig_group.transforms)
def test_join_mesh_flattens_lists(self):
from NodeSlicer.core.nodeslicer.nodes.mesh import SLICENODE_JoinMesh