From 0cf055876c8446e3a5775601e1b026f6da51535a Mon Sep 17 00:00:00 2001 From: Nicolas Predella Date: Wed, 3 Jun 2026 19:44:09 +0300 Subject: [PATCH] separate by attribute --- core/nodeslicer/nodes/mesh.py | 98 ++++++++++++++++++++++++++- core/nodeslicer/tree.py | 1 + infra/blender/blender_mesh_capture.py | 5 +- 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/core/nodeslicer/nodes/mesh.py b/core/nodeslicer/nodes/mesh.py index 6f863f7..8e78d38 100644 --- a/core/nodeslicer/nodes/mesh.py +++ b/core/nodeslicer/nodes/mesh.py @@ -2,9 +2,9 @@ from __future__ import annotations from typing import Any import numpy as np -from ..bpy_ns_types import NfBoolProperty +from ..bpy_ns_types import NfBoolProperty, NfEnumProperty, notify_tree_changed from ....registry import register_class -from .base import SliceNode, SlicingCollection, get_all_children, SliceNodeJoiner, SlicingGroup +from .base import SliceNode, SlicingCollection, get_all_children, SliceNodeJoiner, SlicingGroup, SlicingObject @register_class class SLICENODE_GetMesh(SliceNode): @@ -63,3 +63,97 @@ class SLICENODE_JoinMeshGroups(SliceNodeJoiner): group.collections[coll.objects[0].name] = coll return group + +@register_class +class SLICENODE_SeparateMesh(SliceNode): + bl_label = "Separate Mesh" + + comparison: NfEnumProperty( + name="Comparison", + items=[ + ('EQUAL', "Equal", "=="), + ('NOT_EQUAL', "Not Equal", "!="), + ('GREATER_THAN', "Greater Than", ">"), + ('LESS_THAN', "Less Than", "<"), + ('GREATER_EQUAL', "Greater Equal", ">="), + ('LESS_EQUAL', "Less Equal", "<="), + ], + default='EQUAL', + update=notify_tree_changed + ) # pyright: ignore[reportInvalidTypeForm] + + def init(self, context): + self.add_input("SLICESOCK_Mesh") + self.add_input("SLICESOCK_String", "Attribute") + self.add_input("SLICESOCK_Float", "Value") + self.add_output("SLICESOCK_Mesh", "Selection") + self.add_output("SLICESOCK_Mesh", "Inverted") + + def draw_buttons(self, context, layout): + assert layout + layout.prop(self, "comparison", text="") + + def evaluate_output(self, socket_name: str, **_kwargs) -> SlicingCollection | None: + coll: SlicingCollection | None = self.resolve("Mesh") + attr_name: str = self.resolve("Attribute") + value: float = self.resolve("Value") + + if not coll: return None + + res_selection = SlicingCollection([], coll.name + "_selection") + res_inverted = SlicingCollection([], coll.name + "_inverted") + + for so in coll.objects: + if attr_name not in so.attributes: + # If attribute missing, entire object goes to inverted + res_inverted.objects.append(so) + continue + + attr = so.attributes[attr_name] + vals = attr["values"] + + mask = np.zeros(len(so.mesh), dtype=bool) + if self.comparison == 'EQUAL': mask = vals == value + elif self.comparison == 'NOT_EQUAL': mask = vals != value + elif self.comparison == 'GREATER_THAN': mask = vals > value + elif self.comparison == 'LESS_THAN': mask = vals < value + elif self.comparison == 'GREATER_EQUAL': mask = vals >= value + elif self.comparison == 'LESS_EQUAL': mask = vals <= value + + if np.all(mask): + res_selection.objects.append(so) + elif not np.any(mask): + res_inverted.objects.append(so) + else: + # Split the object + so_sel = SlicingObject() + so_sel.name = so.name + "_sel" + so_sel.parent = so.parent + so_sel.object_type = so.object_type + so_sel.extruder = so.extruder + so_sel.mesh = so.mesh[mask] + for k, v in so.attributes.items(): + so_sel.attributes[k] = { + "domain": v["domain"], + "type": v["type"], + "values": v["values"][mask] + } + res_selection.objects.append(so_sel) + + so_inv = SlicingObject() + so_inv.name = so.name + "_inv" + so_inv.parent = so.parent + so_inv.object_type = so.object_type + so_inv.extruder = so.extruder + so_inv.mesh = so.mesh[~mask] + for k, v in so.attributes.items(): + so_inv.attributes[k] = { + "domain": v["domain"], + "type": v["type"], + "values": v["values"][~mask] + } + res_inverted.objects.append(so_inv) + + if socket_name == "Selection": + return res_selection + return res_inverted diff --git a/core/nodeslicer/tree.py b/core/nodeslicer/tree.py index e664f58..8131bea 100644 --- a/core/nodeslicer/tree.py +++ b/core/nodeslicer/tree.py @@ -496,6 +496,7 @@ class SLICENODE_MT_Geometry(Menu): layout = self.layout assert layout layout.operator("node.add_node", text="Get Mesh").type = "SLICENODE_GetMesh" + layout.operator("node.add_node", text="Separate Mesh").type = "SLICENODE_SeparateMesh" layout.operator("node.add_node", text="Get Paths").type = "SLICENODE_GetPaths" layout.separator() layout.operator("node.add_node", text="Slice Shells").type = "SLICENODE_SliceShells" diff --git a/infra/blender/blender_mesh_capture.py b/infra/blender/blender_mesh_capture.py index c259e9e..2bde37b 100644 --- a/infra/blender/blender_mesh_capture.py +++ b/infra/blender/blender_mesh_capture.py @@ -29,15 +29,16 @@ class SlicingObject(): def __init__(self, obj: Object | None = None, parent: str = "") -> None: if not bpy.context.scene: raise Exception('No scene currently open!') - if obj: self.name = str(obj.name) + self.name = str(obj.name) if obj else "" self.parent = parent self.attributes = {} + self.modifiers = [] depsgraph = bpy.context.evaluated_depsgraph_get() assert bpy.context.scene.unit_settings scene_scale: float = bpy.context.scene.unit_settings.scale_length - if ns := getattr(obj, 'nodeslicer', None): + if obj and (ns := getattr(obj, 'nodeslicer', None)): self.object_type = ns.object_type self.extruder = ns.extruder self.modifiers = [{'param_id': m.field, 'param_value': m.value_str} for m in ns.modifiers]