"""GLB 部件重命名:按查看器标签定位节点,写回 .glb,并同步故障↔部件映射表。 - 标签规则与前端一致(three.js createUniqueName:同名按出现次序 ``_1``、``_2``…), 直接复用 ``tools/remove_nodes.viewer_labels``。 - 名称强制全局唯一(不得与其它任何节点的原始名重复),避免产生后缀歧义。 - 改名会导致其后同名部件的标签整体前移,故同时生成 旧label→新label 映射, 并调用 ``fault_map_service.remap_parts`` 同步 ``data/fault_part_map.csv``。 """ import os import sys import threading from config import MODULE_DIR from services import fault_map_service, model_service _TOOLS_DIR = MODULE_DIR / "tools" if str(_TOOLS_DIR) not in sys.path: sys.path.insert(0, str(_TOOLS_DIR)) from remove_nodes import viewer_labels # noqa: E402 from split_glb import read_glb, write_glb # noqa: E402 _lock = threading.Lock() def rename_part(model: str, label: str, new_name: str) -> dict: new_name = (new_name or "").strip() if not new_name: raise ValueError("名称不能为空") if "#" in new_name: raise ValueError("名称不能包含 #") path = model_service.resolve_model(model) if path is None: raise FileNotFoundError("模型不存在") if path.suffix.lower() != ".glb": raise ValueError("仅支持 .glb 模型") with _lock: gltf, bin_data = read_glb(str(path)) old_labels = viewer_labels(gltf) targets = [i for i, value in old_labels.items() if value == label] if len(targets) != 1: raise LookupError(f"部件标签未唯一匹配:{label}") index = targets[0] for j, node in enumerate(gltf["nodes"]): if j == index: continue if (node.get("name") or "") == new_name: raise ValueError(f"名称已存在:{new_name}") gltf["nodes"][index]["name"] = new_name new_labels = viewer_labels(gltf) mapping = { old_labels[i]: new_labels[i] for i in old_labels if old_labels[i] != new_labels[i] } tmp = str(path) + ".tmp" write_glb(tmp, gltf, bin_data) os.replace(tmp, path) remapped = fault_map_service.remap_parts(model, mapping) return { "node_index": index, "old_label": label, "new_label": new_labels[index], "mapping": mapping, "remapped_rows": remapped, }