# SPDX-License-Identifier: MIT OR GPL-3.0-or-later import json import os from dataclasses import dataclass from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from bmesh.types import BMesh, BMLayerItem from bpy.types import Context, Mesh, Object # UV坐标精度(小数位数) UV_PRECISION = 6 # 材质到网格的映射 MATERIAL_MESH_MAPPING: dict[str, str] = { "N00_000_00_Body_00_SKIN (Instance)": "Body", "N00_000_00_Face_00_SKIN (Instance)": "Face", "N00_000_00_FaceMouth_00_FACE (Instance)": "Face", "N00_000_00_FaceEyeline_00_FACE (Instance)": "Face", "N00_000_00_FaceEyelash_00_FACE (Instance)": "Face", "N00_000_00_FaceBrow_00_FACE (Instance)": "Face", "N00_000_00_hH_00_FACE (Instance)": "Face", "N00_000_00_EyeIris_00_EYE (Instance)": "Face", "N00_000_00_EyeWhite_00_EYE (Instance)": "Face", "N00_000_00_EyeHighlight_00_EYE (Instance)": "Face", "N00_000_00_HairBack_00_HAIR (Instance)": "Body", } # 使用Blender自带tris_convert_to_quads的材质(网格规律,不需要UV匹配) AUTO_CONVERT_MATERIALS: set[str] = { "N00_000_00_FaceBrow_00_FACE (Instance)", } # 处理顺序(先Body后Face) PROCESSING_ORDER: list[str] = [ "N00_000_00_Body_00_SKIN (Instance)", "N00_000_00_HairBack_00_HAIR (Instance)", "N00_000_00_Face_00_SKIN (Instance)", "N00_000_00_FaceMouth_00_FACE (Instance)", "N00_000_00_FaceEyeline_00_FACE (Instance)", "N00_000_00_FaceEyelash_00_FACE (Instance)", "N00_000_00_FaceBrow_00_FACE (Instance)", "N00_000_00_hH_00_FACE (Instance)", "N00_000_00_EyeIris_00_EYE (Instance)", "N00_000_00_EyeWhite_00_EYE (Instance)", "N00_000_00_EyeHighlight_00_EYE (Instance)", ] @dataclass class MaterialInfo: """材质信息数据类""" name: str index: int face_count: int is_skin: bool def find_body_mesh(armature: "Object") -> Optional["Object"]: """ 在Armature的子对象中查找名为"Body"的网格对象 Args: armature: Armature对象 Returns: Body网格对象,如果不存在则返回None """ if armature is None: return None for child in armature.children: if child.type == "MESH" and child.name == "Body": return child return None def find_face_mesh(armature: "Object") -> Optional["Object"]: """ 在Armature的子对象中查找名为"Face"的网格对象 Args: armature: Armature对象 Returns: Face网格对象,如果不存在则返回None """ if armature is None: return None for child in armature.children: if child.type == "MESH" and child.name == "Face": return child return None def get_target_mesh_name(material_name: str) -> Optional[str]: """ 根据材质名称获取目标网格名称 Args: material_name: 材质名称 Returns: 目标网格名称("Body"或"Face"),如果材质不在映射中返回None """ return MATERIAL_MESH_MAPPING.get(material_name) def get_material_index(mesh_obj: "Object", material_name: str) -> Optional[int]: """ 获取材质在网格中的索引 Args: mesh_obj: 网格对象 material_name: 材质名称 Returns: 材质索引,如果不存在返回None """ if mesh_obj is None or mesh_obj.type != "MESH": return None for i, slot in enumerate(mesh_obj.material_slots): if slot.material and slot.material.name == material_name: return i return None # 保留材质关键字常量(不分离的材质:只有SKIN和HAIR) RETAINED_MATERIAL_KEYWORDS = ["SKIN", "HAIR"] def is_skin_material(material_name: str) -> bool: """ 判断材质是否为皮肤材质 Args: material_name: 材质名称 Returns: 如果材质名包含"SKIN"则返回True(不区分大小写) """ return "SKIN" in material_name.upper() def is_retained_material(material_name: str) -> bool: """ 判断材质是否为保留材质(不分离) 只有SKIN和HAIR材质保留在原Body网格中。 CLOTH和其他材质都需要分离。 Args: material_name: 材质名称 Returns: 如果材质名包含"SKIN"或"HAIR"则返回True(不区分大小写) """ upper_name = material_name.upper() return any(keyword in upper_name for keyword in RETAINED_MATERIAL_KEYWORDS) def is_separable_material(material_name: str) -> bool: """ 判断材质是否为可分离材质 不包含"SKIN"、"HAIR"的材质都是可分离材质, 包括CLOTH和其他所有材质。 Args: material_name: 材质名称 Returns: 如果材质名不包含"SKIN"、"HAIR"则返回True(包括CLOTH和其他材质) """ return not is_retained_material(material_name) def get_material_face_count(mesh: "Mesh", material_index: int) -> int: """ 获取指定材质的面数量 Args: mesh: 网格数据 material_index: 材质索引 Returns: 使用该材质的面数量 """ count = 0 for polygon in mesh.polygons: if polygon.material_index == material_index: count += 1 return count def generate_unique_name(base_name: str, existing_names: set[str]) -> str: """ 生成唯一的对象名称 Args: base_name: 基础名称 existing_names: 已存在的名称集合 Returns: 唯一的名称 """ if base_name not in existing_names: return base_name suffix = 1 while True: candidate = f"{base_name}.{suffix:03d}" if candidate not in existing_names: return candidate suffix += 1 def normalize_uv_pair( uv1: tuple[float, float], uv2: tuple[float, float] ) -> tuple[tuple[float, float], tuple[float, float]]: """ 标准化UV坐标对(较小的在前) 通过将较小的UV坐标放在前面,确保相同的边无论顶点顺序如何都能匹配。 使用6位小数精度进行比较和存储。 Args: uv1: 第一个UV坐标 (u, v) uv2: 第二个UV坐标 (u, v) Returns: 标准化后的UV对 ((u1,v1), (u2,v2)),较小的坐标在前 """ # 四舍五入到指定精度 uv1_rounded = (round(uv1[0], UV_PRECISION), round(uv1[1], UV_PRECISION)) uv2_rounded = (round(uv2[0], UV_PRECISION), round(uv2[1], UV_PRECISION)) # 比较并排序,较小的在前 if uv1_rounded <= uv2_rounded: return (uv1_rounded, uv2_rounded) return (uv2_rounded, uv1_rounded) def load_edge_uv_data(material_name: str) -> Optional[set[tuple]]: """ 从JSON文件加载边UV数据 Args: material_name: 材质名称(用于定位JSON文件) Returns: UV边坐标集合,格式为 {((u1,v1), (u2,v2)), ...} 如果文件不存在返回None """ # 获取当前模块所在目录 current_dir = os.path.dirname(os.path.abspath(__file__)) json_path = os.path.join(current_dir, f"{material_name}.json") if not os.path.exists(json_path): return None try: with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) edge_uvs = data.get("edge_uvs", []) result: set[tuple] = set() for edge_uv in edge_uvs: if len(edge_uv) == 2 and len(edge_uv[0]) == 2 and len(edge_uv[1]) == 2: uv1 = (float(edge_uv[0][0]), float(edge_uv[0][1])) uv2 = (float(edge_uv[1][0]), float(edge_uv[1][1])) normalized = normalize_uv_pair(uv1, uv2) result.add(normalized) return result except (json.JSONDecodeError, KeyError, TypeError, ValueError): return None def select_edges_by_uv( bm: "BMesh", uv_layer: "BMLayerItem", target_uvs: set[tuple], ) -> int: """ 根据UV坐标选中边 遍历BMesh中的所有边,比较每条边的loop UV坐标与目标UV集合, 选中匹配的边。使用6位小数精度进行匹配。 Args: bm: BMesh对象 uv_layer: UV层 target_uvs: 目标UV坐标集合,格式为 {((u1,v1), (u2,v2)), ...} Returns: 选中的边数量 """ selected_count = 0 for edge in bm.edges: # 获取边的两个顶点关联的loop # 每条边可能有多个关联的面,我们需要检查所有关联的loop edge_selected = False for face in edge.link_faces: # 找到这条边在当前面中的两个loop edge_loops = [] for loop in face.loops: if loop.vert in edge.verts: edge_loops.append(loop) if len(edge_loops) == 2: # 获取两个loop的UV坐标 uv1_data = edge_loops[0][uv_layer] uv2_data = edge_loops[1][uv_layer] uv1 = (uv1_data.uv[0], uv1_data.uv[1]) uv2 = (uv2_data.uv[0], uv2_data.uv[1]) # 标准化UV对并检查是否在目标集合中 normalized = normalize_uv_pair(uv1, uv2) if normalized in target_uvs: edge_selected = True break if edge_selected: edge.select = True selected_count += 1 return selected_count def isolate_material_faces( context: "Context", mesh_obj: "Object", material_name: str, ) -> bool: """ 隔离显示指定材质的面(隐藏其他面) 进入编辑模式,选择指定材质的所有面,执行隐藏未选中操作, 然后切换到边选择模式。 Args: context: Blender上下文 mesh_obj: 网格对象 material_name: 材质名称 Returns: 是否成功隔离 Requirements: 3.1, 3.2, 3.3, 3.4 """ import bpy if mesh_obj is None or mesh_obj.type != "MESH": return False # 获取材质索引 material_index = get_material_index(mesh_obj, material_name) if material_index is None: return False # 保存当前活动对象和选择状态 original_active = context.view_layer.objects.active original_mode = context.object.mode if context.object else "OBJECT" try: # 确保在对象模式下开始 if original_mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") # 取消所有选择,选择目标网格 bpy.ops.object.select_all(action="DESELECT") mesh_obj.select_set(True) context.view_layer.objects.active = mesh_obj # 进入编辑模式 (Requirements 3.1) bpy.ops.object.mode_set(mode="EDIT") # 切换到面选择模式 bpy.ops.mesh.select_mode(type="FACE") # 取消所有选择 bpy.ops.mesh.select_all(action="DESELECT") # 设置活动材质索引并选择该材质的所有面 (Requirements 3.2) mesh_obj.active_material_index = material_index bpy.ops.object.material_slot_select() # 执行隐藏未选中 (Shift+H equivalent) (Requirements 3.3) bpy.ops.mesh.hide(unselected=True) # 切换到边选择模式 (Requirements 3.4) bpy.ops.mesh.select_mode(type="EDGE") return True except Exception: # 发生错误时尝试恢复状态 try: if context.object and context.object.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT") if original_active: context.view_layer.objects.active = original_active except Exception: pass return False def select_material_faces( mesh_obj: "Object", material_index: int, ) -> set[int]: """ 获取指定材质索引的所有面的索引集合 这是一个纯函数,用于测试材质面选择的完整性。 不修改任何Blender状态。 Args: mesh_obj: 网格对象 material_index: 材质索引 Returns: 使用该材质的面索引集合 """ if mesh_obj is None or mesh_obj.type != "MESH": return set() mesh = mesh_obj.data result: set[int] = set() for i, polygon in enumerate(mesh.polygons): if polygon.material_index == material_index: result.add(i) return result