# SPDX-License-Identifier: MIT OR GPL-3.0-or-later from collections.abc import Set as AbstractSet from dataclasses import dataclass from typing import TYPE_CHECKING, Optional import bpy from bpy.types import Context, Operator from .utils import ( AUTO_CONVERT_MATERIALS, PROCESSING_ORDER, MaterialInfo, find_body_mesh, find_face_mesh, generate_unique_name, get_material_face_count, get_target_mesh_name, is_retained_material, is_separable_material, is_skin_material, isolate_material_faces, load_edge_uv_data, select_edges_by_uv, ) if TYPE_CHECKING: from bpy.types import Object @dataclass class ProcessResult: """材质处理结果""" material_name: str mesh_name: str edges_selected: int edges_dissolved: int success: bool error_message: Optional[str] = None @dataclass class MergeOperationState: """操作状态,用于错误恢复""" original_mode: str original_active_object: Optional["Object"] original_selected_objects: list["Object"] processed_materials: list[str] 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网格中可分离材质(CLOTH和其他)的面合并分离为单个独立对象" 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 # 检查是否有可分离材质(使用is_separable_material) mesh_data = body_mesh.data if mesh_data is None: return False for material in mesh_data.materials: if material is not None and is_separable_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"} # 收集可分离材质信息(使用is_separable_material) separable_materials: list[MaterialInfo] = [] for i, material in enumerate(mesh_data.materials): if material is not None and is_separable_material(material.name): face_count = get_material_face_count(mesh_data, i) if face_count > 0: separable_materials.append( MaterialInfo( name=material.name, index=i, face_count=face_count, is_skin=False, ) ) if not separable_materials: self.report({"INFO"}, "没有找到可分离材质的面") return {"FINISHED"} # 获取现有对象名称集合 existing_names = set(obj.name for obj in context.blend_data.objects) # 将所有可分离材质的面合并分离为单个对象 material_indices = [mat_info.index for mat_info in separable_materials] new_obj = self._separate_all_material_faces( context, body_mesh, armature, material_indices, existing_names ) if new_obj is not None: total_faces = sum(mat_info.face_count for mat_info in separable_materials) material_names = [mat_info.name for mat_info in separable_materials] self.report( {"INFO"}, f"成功分离了 {len(separable_materials)} 个材质的 {total_faces} 个面到 {new_obj.name}" ) return {"FINISHED"} else: self.report({"WARNING"}, "没有成功分离任何面") return {"CANCELLED"} def _separate_all_material_faces( self, context: Context, body_obj: "Object", armature: "Object", material_indices: list[int], 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") # 选择所有使用可分离材质的面 material_index_set = set(material_indices) for polygon in mesh_data.polygons: if polygon.material_index in material_index_set: 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 # 使用固定名称"Body_Separated",处理命名冲突 base_name = "Body_Separated" 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 class VRM_OT_merge_uv_edges(Operator): """根据UV坐标融并边,将三角面转四边面""" bl_idname = "vrm.merge_uv_edges" bl_label = "Merge UV Edges" bl_description = "根据UV坐标选中并融并边,将三角面转换为四边面" bl_options: AbstractSet[str] = {"REGISTER", "UNDO"} @classmethod def poll(cls, context: Context) -> bool: """ 检查是否可以执行操作 Requirements 1.1, 1.2, 1.3, 1.4: - 检测Armature对象或其子网格对象 - 检测Body和Face网格是否存在 - 在任意Blender模式下工作 """ armature = cls._get_armature(context) if armature is None: return False # Requirements 1.1, 1.2: 检查Body和Face网格是否都存在 body_mesh = find_body_mesh(armature) face_mesh = find_face_mesh(armature) return body_mesh is not None and face_mesh is not None @classmethod def _get_armature(cls, context: Context) -> Optional["Object"]: """ 获取当前选中的Armature对象 Requirements 1.1: 支持选中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]: """ 执行边融并操作 Requirements 5.3, 5.4, 6.1, 6.2: - 保存原始状态(模式、选择、活动对象) - 按PROCESSING_ORDER遍历材质 - 调用_process_material处理每个材质 - 恢复状态并报告结果 """ # 保存原始状态 state = self._save_state(context) try: return self._execute_merge(context, state) except Exception as e: # 发生错误时恢复状态 self._restore_state(context, state) self.report({"ERROR"}, f"边融并操作失败: {str(e)}") return {"CANCELLED"} def _save_state(self, context: Context) -> MergeOperationState: """保存当前Blender状态""" original_mode = context.object.mode if context.object else "OBJECT" original_active = context.view_layer.objects.active original_selected = list(context.selected_objects) return MergeOperationState( original_mode=original_mode, original_active_object=original_active, original_selected_objects=original_selected, processed_materials=[], ) def _restore_state(self, context: Context, state: MergeOperationState) -> None: """ 恢复原始Blender状态 Requirements 7.4: 错误时恢复原始状态 """ try: # 确保在对象模式 if context.object and context.object.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") # 取消所有选择 bpy.ops.object.select_all(action="DESELECT") # 恢复原始选择 for obj in state.original_selected_objects: if obj and obj.name in context.blend_data.objects: obj.select_set(True) # 恢复原始活动对象 if state.original_active_object and \ state.original_active_object.name in context.blend_data.objects: context.view_layer.objects.active = state.original_active_object # 恢复原始模式 if context.object and state.original_mode != "OBJECT": try: bpy.ops.object.mode_set(mode=state.original_mode) except RuntimeError: pass # 某些模式可能无法恢复 except Exception: pass # 恢复失败时静默处理 def _execute_merge( self, context: Context, state: MergeOperationState ) -> set[str]: """ 执行边融并的核心逻辑 Requirements 6.1, 6.2: 按顺序处理材质,先Body后Face """ armature = self._get_armature(context) if armature is None: self.report({"ERROR"}, "未找到Armature对象") return {"CANCELLED"} body_mesh = find_body_mesh(armature) face_mesh = find_face_mesh(armature) if body_mesh is None or face_mesh is None: self.report({"ERROR"}, "未找到Body或Face网格对象") return {"CANCELLED"} # 确保在对象模式开始 if context.object and context.object.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") # 处理结果统计 results: list[ProcessResult] = [] total_edges_dissolved = 0 materials_processed = 0 materials_skipped = 0 # 按PROCESSING_ORDER遍历材质 (Requirements 6.1, 6.2) for material_name in PROCESSING_ORDER: # 确定目标网格 target_mesh_name = get_target_mesh_name(material_name) if target_mesh_name is None: continue target_mesh = body_mesh if target_mesh_name == "Body" else face_mesh # 处理材质 result = self._process_material(context, target_mesh, material_name) results.append(result) state.processed_materials.append(material_name) if result.success: materials_processed += 1 total_edges_dissolved += result.edges_dissolved else: materials_skipped += 1 if result.error_message: self.report({"WARNING"}, result.error_message) # 确保返回对象模式 (Requirements 5.4) if context.object and context.object.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") # 报告结果 (Requirements 6.4) if materials_processed > 0: self.report( {"INFO"}, f"处理完成: {materials_processed} 个材质, " f"融并 {total_edges_dissolved} 条边" ) return {"FINISHED"} else: self.report({"WARNING"}, f"没有成功处理任何材质 (跳过 {materials_skipped} 个)") return {"CANCELLED"} def _process_material( self, context: Context, mesh_obj: "Object", material_name: str, ) -> ProcessResult: """ 处理单个材质的边融并 Requirements 5.1, 5.2, 6.3, 7.1, 7.2: - 确定目标网格(Body或Face) - 加载JSON边数据 - 调用isolate_material_faces隔离材质 - 调用select_edges_by_uv选择边 - 执行dissolve_edges融并 - 取消隐藏(Alt+H) - 处理缺失JSON文件 (7.1) - 处理无匹配边情况 (7.2) """ import bmesh mesh_name = mesh_obj.name if mesh_obj else "Unknown" # 检查是否使用自动转换(tris_convert_to_quads) if material_name in AUTO_CONVERT_MATERIALS: return self._process_material_auto_convert(context, mesh_obj, material_name) try: # 加载JSON边数据 (Requirements 5.1, 7.1) target_uvs = load_edge_uv_data(material_name) if target_uvs is None: # Requirements 7.1: 记录警告并跳过 return ProcessResult( material_name=material_name, mesh_name=mesh_name, edges_selected=0, edges_dissolved=0, success=False, error_message=f"跳过: 未找到材质 {material_name} 的JSON文件", ) if len(target_uvs) == 0: return ProcessResult( material_name=material_name, mesh_name=mesh_name, edges_selected=0, edges_dissolved=0, success=False, error_message=f"跳过: 材质 {material_name} 的JSON文件中没有边数据", ) # 隔离材质面 (Requirements 5.2) if not isolate_material_faces(context, mesh_obj, material_name): return ProcessResult( material_name=material_name, mesh_name=mesh_name, edges_selected=0, edges_dissolved=0, success=False, error_message=f"跳过: 无法隔离材质 {material_name} 的面", ) # 此时应该在编辑模式,边选择模式 # 取消所有边选择 bpy.ops.mesh.select_all(action="DESELECT") # 使用BMesh选择边 mesh_data = mesh_obj.data bm = bmesh.from_edit_mesh(mesh_data) # 获取UV层 uv_layer = bm.loops.layers.uv.active if uv_layer is None: # 取消隐藏并返回 bpy.ops.mesh.reveal() bpy.ops.object.mode_set(mode="OBJECT") return ProcessResult( material_name=material_name, mesh_name=mesh_name, edges_selected=0, edges_dissolved=0, success=False, error_message=f"跳过: 网格 {mesh_name} 没有UV层", ) # 选择匹配UV的边 (Requirements 5.2) edges_selected = select_edges_by_uv(bm, uv_layer, target_uvs) # 更新BMesh bmesh.update_edit_mesh(mesh_data) # Requirements 7.2: 处理无匹配边情况 if edges_selected == 0: # 取消隐藏并返回 bpy.ops.mesh.reveal() bpy.ops.object.mode_set(mode="OBJECT") return ProcessResult( material_name=material_name, mesh_name=mesh_name, edges_selected=0, edges_dissolved=0, success=False, error_message=( f"跳过: 材质 {material_name} 没有找到匹配的边 " f"(目标边数: {len(target_uvs)})" ), ) # 执行边融并 (Requirements 5.1) edges_dissolved = 0 try: bpy.ops.mesh.dissolve_edges() edges_dissolved = edges_selected except RuntimeError as e: # 融并失败,可能是边不可融并,但不算完全失败 self.report({"WARNING"}, f"材质 {material_name} 融并边时出现问题: {str(e)}") # 取消隐藏 (Alt+H equivalent) (Requirements 5.2) bpy.ops.mesh.reveal() # 返回对象模式 (Requirements 6.3) bpy.ops.object.mode_set(mode="OBJECT") return ProcessResult( material_name=material_name, mesh_name=mesh_name, edges_selected=edges_selected, edges_dissolved=edges_dissolved, success=True, error_message=None, ) except Exception as e: # Requirements 7.4: 捕获异常,尝试恢复状态 try: # 尝试取消隐藏 if context.object and context.object.mode == "EDIT": bpy.ops.mesh.reveal() bpy.ops.object.mode_set(mode="OBJECT") except Exception: pass return ProcessResult( material_name=material_name, mesh_name=mesh_name, edges_selected=0, edges_dissolved=0, success=False, error_message=f"处理材质 {material_name} 时发生错误: {str(e)}", ) def _process_material_auto_convert( self, context: Context, mesh_obj: "Object", material_name: str, ) -> ProcessResult: """ 使用Blender自带的tris_convert_to_quads处理规律网格的材质 对于网格结构规律的材质,直接使用Blender的自动转换功能, 不需要基于UV坐标的边选择。 """ mesh_name = mesh_obj.name if mesh_obj else "Unknown" try: # 隔离材质面 if not isolate_material_faces(context, mesh_obj, material_name): return ProcessResult( material_name=material_name, mesh_name=mesh_name, edges_selected=0, edges_dissolved=0, success=False, error_message=f"跳过: 无法隔离材质 {material_name} 的面", ) # 切换到面选择模式 bpy.ops.mesh.select_mode(type="FACE") # 选择所有可见的面 bpy.ops.mesh.select_all(action="SELECT") # 使用Blender自带的三角面转四边面 bpy.ops.mesh.tris_convert_to_quads() # 取消隐藏 bpy.ops.mesh.reveal() # 返回对象模式 bpy.ops.object.mode_set(mode="OBJECT") return ProcessResult( material_name=material_name, mesh_name=mesh_name, edges_selected=0, edges_dissolved=0, success=True, error_message=None, ) except Exception as e: # 捕获异常,尝试恢复状态 try: if context.object and context.object.mode == "EDIT": bpy.ops.mesh.reveal() bpy.ops.object.mode_set(mode="OBJECT") except Exception: pass return ProcessResult( material_name=material_name, mesh_name=mesh_name, edges_selected=0, edges_dissolved=0, success=False, error_message=f"处理材质 {material_name} 时发生错误: {str(e)}", )