# SPDX-License-Identifier: MIT OR GPL-3.0-or-later from collections.abc import Set as AbstractSet from typing import TYPE_CHECKING, Optional import bpy from bpy.types import Context, Operator from .utils import ( MaterialInfo, find_body_mesh, generate_unique_name, get_material_face_count, is_skin_material, ) if TYPE_CHECKING: from bpy.types import Object class VRM_OT_separate_non_skin_faces(Operator): """将Body网格中非皮肤材质的面分离为独立对象""" bl_idname = "vrm.separate_non_skin_faces" bl_label = "Separate Non-Skin Faces" bl_description = "将Body网格中非皮肤材质的面分离为独立对象" bl_options: AbstractSet[str] = {"REGISTER", "UNDO"} @classmethod def poll(cls, context: Context) -> bool: """检查是否可以执行分离操作""" armature = cls._get_armature(context) if armature is None: return False body_mesh = find_body_mesh(armature) if body_mesh is None: return False # 检查是否有非皮肤材质 mesh_data = body_mesh.data if mesh_data is None: return False for material in mesh_data.materials: if material is not None and not is_skin_material(material.name): return True return False @classmethod def _get_armature(cls, context: Context) -> Optional["Object"]: """获取当前选中的Armature对象""" active_object = context.active_object if active_object is None: return None if active_object.type == "ARMATURE": return active_object # 如果选中的是网格,检查其父对象是否为Armature if active_object.parent is not None and active_object.parent.type == "ARMATURE": return active_object.parent return None def execute(self, context: Context) -> set[str]: """执行分离操作""" try: return self._execute_separation(context) except Exception as e: self.report({"ERROR"}, f"分离操作失败: {str(e)}") return {"CANCELLED"} def _execute_separation(self, context: Context) -> set[str]: """执行分离操作的核心逻辑""" armature = self._get_armature(context) if armature is None: self.report({"ERROR"}, "未找到Armature对象") return {"CANCELLED"} body_mesh = find_body_mesh(armature) if body_mesh is None: self.report({"ERROR"}, "未找到Body网格对象") return {"CANCELLED"} mesh_data = body_mesh.data if mesh_data is None: self.report({"ERROR"}, "Body网格数据无效") return {"CANCELLED"} # 收集非皮肤材质信息 non_skin_materials: list[MaterialInfo] = [] for i, material in enumerate(mesh_data.materials): if material is not None and not is_skin_material(material.name): face_count = get_material_face_count(mesh_data, i) if face_count > 0: non_skin_materials.append( MaterialInfo( name=material.name, index=i, face_count=face_count, is_skin=False, ) ) if not non_skin_materials: self.report({"INFO"}, "没有找到非皮肤材质的面") return {"FINISHED"} # 获取现有对象名称集合 existing_names = set(obj.name for obj in context.blend_data.objects) # 为每个非皮肤材质执行分离 separated_count = 0 for mat_info in non_skin_materials: result = self._separate_material_faces( context, body_mesh, armature, mat_info, existing_names ) if result: separated_count += 1 existing_names.add(result.name) if separated_count > 0: self.report({"INFO"}, f"成功分离了 {separated_count} 个材质的面") return {"FINISHED"} else: self.report({"WARNING"}, "没有成功分离任何面") return {"CANCELLED"} def _separate_material_faces( self, context: Context, body_obj: "Object", armature: "Object", mat_info: MaterialInfo, existing_names: set[str], ) -> Optional["Object"]: """分离指定材质的面为新对象""" mesh_data = body_obj.data if mesh_data is None: return None # 确保在对象模式 if context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") # 取消所有选择 bpy.ops.object.select_all(action="DESELECT") # 选择Body网格并设为活动对象 body_obj.select_set(True) context.view_layer.objects.active = body_obj # 进入编辑模式 bpy.ops.object.mode_set(mode="EDIT") # 取消所有选择 bpy.ops.mesh.select_all(action="DESELECT") # 切换到面选择模式 bpy.ops.mesh.select_mode(type="FACE") # 返回对象模式以访问网格数据 bpy.ops.object.mode_set(mode="OBJECT") # 选择使用指定材质的面 for polygon in mesh_data.polygons: if polygon.material_index == mat_info.index: polygon.select = True # 返回编辑模式执行分离 bpy.ops.object.mode_set(mode="EDIT") # 执行分离操作 bpy.ops.mesh.separate(type="SELECTED") # 返回对象模式 bpy.ops.object.mode_set(mode="OBJECT") # 查找新创建的对象 new_obj = self._find_new_separated_object(context, body_obj) if new_obj is None: return None # 生成唯一名称 base_name = self._generate_object_name(mat_info.name) unique_name = generate_unique_name(base_name, existing_names) new_obj.name = unique_name # 设置父子关系(保持与原Armature的关系) new_obj.parent = armature new_obj.parent_type = "OBJECT" return new_obj def _find_new_separated_object( self, context: Context, original_obj: "Object" ) -> Optional["Object"]: """查找分离操作后新创建的对象""" # 分离后,新对象会被选中,原对象也会被选中 for obj in context.selected_objects: if obj != original_obj and obj.type == "MESH": return obj return None def _generate_object_name(self, material_name: str) -> str: """根据材质名称生成对象名称""" # 简化材质名称,移除常见前缀和后缀 simplified_name = material_name # 移除 "(Instance)" 后缀 if simplified_name.endswith(" (Instance)"): simplified_name = simplified_name[:-11] # 移除常见的VRM材质前缀 prefixes_to_remove = ["N00_000_00_", "N00_001_00_", "N00_002_00_"] for prefix in prefixes_to_remove: if simplified_name.startswith(prefix): simplified_name = simplified_name[len(prefix) :] break return f"Body_{simplified_name}"