#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ SUW Core - Deletion Manager Module 拆分自: suw_impl.py (Line 4274-4800, 6970-7100) 用途: Blender删除管理、对象清理、数据结构维护 版本: 1.0.0 作者: SUWood Team """ from .memory_manager import memory_manager, execute_in_main_thread from .data_manager import data_manager, get_data_manager import time import logging import threading import sys from typing import Dict, Any, List, Optional # 配置日志系统 def setup_logging(): """配置日志系统,确保在Blender控制台中能看到输出""" try: # 获取根日志记录器 root_logger = logging.getLogger() # 如果已经有处理器,不重复配置 if root_logger.handlers: return # 设置日志级别 root_logger.setLevel(logging.INFO) # 创建控制台处理器 console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.INFO) # 创建格式化器 formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%H:%M:%S' ) console_handler.setFormatter(formatter) # 添加处理器到根日志记录器 root_logger.addHandler(console_handler) # 特别配置SUW相关的日志记录器 suw_logger = logging.getLogger('suw_core') suw_logger.setLevel(logging.INFO) print("✅ 日志系统配置完成") except Exception as e: print(f"❌ 日志系统配置失败: {e}") # 在模块加载时自动配置日志 setup_logging() # 设置日志 logger = logging.getLogger(__name__) # 检查Blender可用性 try: import bpy BLENDER_AVAILABLE = True except ImportError: BLENDER_AVAILABLE = False # ==================== 删除管理器类 ==================== class DeletionManager: """删除管理器 - 负责所有删除相关操作""" def __init__(self): """ 初始化删除管理器 - 完全独立,不依赖suw_impl """ # 使用全局数据管理器 self.data_manager = get_data_manager() # 【修复】删除统计 - 添加缺失的键 self.deletion_stats = { "units_deleted": 0, "zones_deleted": 0, "parts_deleted": 0, "hardwares_deleted": 0, "objects_deleted": 0, "deletion_errors": 0, "total_deletions": 0, # 添加这个键 "successful_deletions": 0, # 添加这个键 "failed_deletions": 0 # 添加这个键 } logger.info("✅ 删除管理器初始化完成") # ==================== 原始命令方法 ==================== def c09(self, data: Dict[str, Any]): """c09 - 删除实体 - 与Ruby版本保持一致的逻辑""" try: if not BLENDER_AVAILABLE: logger.warning("Blender 不可用,跳过删除操作") return logger.info("️ 执行c09命令: 删除实体") uid = data.get("uid") typ = data.get("typ") oid = data.get("oid") if not uid or not typ or oid is None: logger.error("缺少必要参数: uid, typ, oid") return logger.info(f"🗑️ 删除参数: uid={uid}, typ={typ}, oid={oid}") # 【修复】与Ruby版本保持一致:先清除所有选择 self._clear_selection() def delete_entities(): """安全地删除实体,修复了重复删除导致崩溃的问题""" try: self.data_manager.data = data deleted_count = 0 if typ == "wall": logger.info(f"🗑️ 删除墙体: uid={uid}, wall={oid}") result = self._del_wall_entity_safe(data, uid, oid) deleted_count = result if result is not None else 0 elif typ == "zid": logger.info(f"🗑️ 删除区域及其内容: uid={uid}, zid={oid}") result = self._del_zone_complete(uid, oid) deleted_count += result if result is not None else 0 # 清理数据结构 - 不再重新删除Blender对象 logger.info(f"🧹 清理区域关联的Parts数据...") parts = self.data_manager.get_parts(data) parts_to_remove = [ cp for cp, part in parts.items() if self._is_object_valid(part) and part.get("sw_zid") == oid ] for cp in parts_to_remove: self.data_manager.remove_part(uid, cp) logger.info(f"🧹 清理区域关联的Hardwares数据...") hardwares = self.data_manager.get_hardwares(data) hardwares_to_remove = [ hw_id for hw_id, hw in hardwares.items() if self._is_object_valid(hw) and hw.get("sw_zid") == oid ] for hw_id in hardwares_to_remove: self.data_manager.remove_hardware(uid, hw_id) elif typ == "cp": logger.info(f"🗑️ 删除部件: uid={uid}, cp={oid}") result = self._del_part_complete(uid, oid) deleted_count += result if result is not None else 0 else: # typ == "uid" 或其他过滤条件 logger.info(f"🗑️ 按类型 '{typ}' 和 oid '{oid}' 删除实体") zones = self.data_manager.get_zones(data) deleted_count += self._del_entities_by_type( zones, typ, oid, uid) parts = self.data_manager.get_parts(data) deleted_count += self._del_entities_by_type( parts, typ, oid, uid) hardwares = self.data_manager.get_hardwares(data) deleted_count += self._del_entities_by_type( hardwares, typ, oid, uid) # 容器级别删除后的通用清理 if typ in ["uid", "zid"]: self._clear_labels_safe(uid) self._clear_door_labels_safe(uid) logger.info(f"✅ 清理labels完成") self._cleanup_machinings(uid) logger.info(f"✅ 清理machinings完成") # 顶级(unit)删除后的特殊清理 if typ == "uid": # 重置材质类型为正常 if hasattr(self.data_manager, 'mat_type'): self.data_manager.mat_type = 0 # MAT_TYPE_NORMAL # 删除尺寸标注 self._del_dimensions(data) logger.info(f"✅ 重置材质类型并删除尺寸标注") # 清理临时存储的data self.data_manager.data = None logger.info(f"✅ c09删除完成: 共处理约 {deleted_count} 个对象") return deleted_count except Exception as e: logger.error(f"❌ c09删除失败: {e}") import traceback logger.error(traceback.format_exc()) return 0 # 执行删除 deleted_count = delete_entities() # 【修复】确保deleted_count是数字 if deleted_count is None: deleted_count = 0 # 当未找到匹配对象时,跳过清理数据和删除对象操作 if deleted_count == 0: logger.info( f"ℹ️ 未找到匹配的对象,跳过清理操作: uid={uid}, typ={typ}, oid={oid}") # 即使没有删除任何对象,也认为是成功的(对象本来就不存在) self.deletion_stats["total_deletions"] += 1 self.deletion_stats["successful_deletions"] += 1 logger.info(f"✅ 命令 c09 执行成功,未找到匹配对象") return True # 修复:返回True # 更新统计 self.deletion_stats["total_deletions"] += 1 if deleted_count > 0: self.deletion_stats["successful_deletions"] += 1 else: self.deletion_stats["failed_deletions"] += 1 logger.info(f"✅ 命令 c09 执行成功,删除 {deleted_count} 个对象") return True # 修复:返回True except Exception as e: logger.error(f"❌ c09命令异常: {e}") import traceback logger.error(traceback.format_exc()) def c03(self, data: Dict[str, Any]): """add_zone - 添加区域 - 修复版本:直接创建六个面组成Zone""" try: if not BLENDER_AVAILABLE: logger.warning("Blender 不可用,跳过区域创建") return None logger.info("️ 执行c03命令: 添加区域") uid = data.get("uid") zid = data.get("zid") elements = data.get("children", []) logger.info(f" Zone_{uid} 数据: uid={uid}, 元素数量={len(elements)}") # 【修复】不再创建线框立方体,直接创建六个面组成Zone # 创建一个空的组对象作为Zone容器 group = bpy.data.objects.new(f"Zone_{uid}", None) group.empty_display_type = 'PLAIN_AXES' # 改为PLAIN_AXES,不显示线框 bpy.context.scene.collection.objects.link(group) # 【修复】确保属性只设置一次,避免重复 group["sw_uid"] = uid group["sw_zid"] = zid # 只设置一次 group["sw_zip"] = data.get("zip", -1) group["sw_typ"] = "zone" # 改为"zone"而不是"zid" # 【调试】打印设置的属性 logger.info(f"🔧 Zone_{uid} 属性设置:") logger.info(f" sw_uid: {group.get('sw_uid')}") logger.info(f" sw_zid: {group.get('sw_zid')}") logger.info(f" sw_zip: {group.get('sw_zip')}") logger.info(f" sw_typ: {group.get('sw_typ')}") if "cor" in data: group["sw_cor"] = data["cor"] # 为每个元素创建面 created_faces = [] failed_faces = [] for i, element in enumerate(elements): surf = element.get("surf", {}) child = element.get("child") p_value = surf.get("p", 0) f_value = surf.get("f", 0) logger.info( f"🔧 处理元素 {i+1}/{len(elements)}: child={child}, p={p_value}, f={f_value}") logger.info(f" 顶点数据: {surf.get('segs', [])}") # 创建面 face = self._create_face_safe(group, surf) if face: # 设置面的属性 face["sw_child"] = child face["sw_uid"] = uid face["sw_zid"] = zid face["sw_typ"] = "face" face["sw_p"] = p_value face["sw_f"] = f_value # 如果是门板层(p=1),设置到门板层 if p_value == 1: face["sw_door_layer"] = True created_faces.append(face) logger.info( f"✅ 创建面成功: {face.name}, child={child}, p={p_value}") else: failed_faces.append((child, p_value, f_value)) logger.error( f"❌ 创建面失败: child={child}, p={p_value}, f={f_value}") # 记录创建的面数量 group["sw_created_faces"] = len(created_faces) group["sw_failed_faces"] = len(failed_faces) logger.info(f"📊 Zone_{uid} 创建统计:") logger.info(f" 成功创建: {len(created_faces)} 个面") logger.info(f" 创建失败: {len(failed_faces)} 个面") if failed_faces: logger.info(f" 失败的面: {failed_faces}") # 应用单元变换(如果存在) if hasattr(self.data_manager, 'unit_trans') and uid in self.data_manager.unit_trans: unit_trans = self.data_manager.unit_trans[uid] group.matrix_world @= unit_trans # 【修复】使用data_manager.add_zone()方法存储数据,而不是直接操作字典 self.data_manager.add_zone(uid, zid, group) logger.info( f"✅ 使用data_manager.add_zone()存储区域数据: uid={uid}, zid={zid}") logger.info( f"✅ 区域创建完成: uid={uid}, zid={zid}, 面数={len(created_faces)}") return group except Exception as e: logger.error(f"c03命令执行失败: {e}") self.deletion_stats["deletion_errors"] += 1 import traceback logger.error(traceback.format_exc()) return None def _parse_transformation(self, trans_data: Dict[str, str]): """解析变换矩阵""" try: import bpy from mathutils import Matrix, Vector # 解析原点 o_str = trans_data.get("o", "(0,0,0)") o = self._parse_point3d(o_str) # 解析轴向量 x_str = trans_data.get("x", "(1,0,0)") y_str = trans_data.get("y", "(0,1,0)") z_str = trans_data.get("z", "(0,0,1)") x = self._parse_vector3d(x_str) y = self._parse_vector3d(y_str) z = self._parse_vector3d(z_str) # 创建变换矩阵 trans_matrix = Matrix(( (x.x, y.x, z.x, o.x), (x.y, y.y, z.y, o.y), (x.z, y.z, z.z, o.z), (0, 0, 0, 1) )) return trans_matrix except Exception as e: logger.error(f"解析变换矩阵失败: {e}") return Matrix.Identity(4) def _parse_point3d(self, point_str: str): """解析3D点 - 修复尺寸比例问题""" try: from mathutils import Vector # 移除括号并分割 point_str = point_str.strip("()") coords = point_str.split(",") x = float(coords[0].strip()) * 0.001 # 转换为米(Blender使用米作为单位) y = float(coords[1].strip()) * 0.001 # 转换为米 z = float(coords[2].strip()) * 0.001 # 转换为米 return Vector((x, y, z)) except Exception as e: logger.error(f"解析3D点失败: {e}") from mathutils import Vector return Vector((0, 0, 0)) def _parse_vector3d(self, vector_str: str): """解析3D向量""" return self._parse_point3d(vector_str) def _create_face_safe(self, container, surface): """安全创建面 - 使用Blender的正确方式""" try: import bpy from mathutils import Vector segs = surface.get("segs", []) if not segs: logger.warning("面数据中没有segs信息") return None logger.info(f"🔧 开始创建面,segs数量: {len(segs)}") # 解析所有顶点 vertices = [] for i, seg in enumerate(segs): if len(seg) >= 2: start = self._parse_point3d(seg[0]) vertices.append(start) logger.debug(f" 顶点 {i}: {seg[0]} -> {start}") logger.info(f"📊 解析得到 {len(vertices)} 个顶点") # 去重并保持顺序 unique_vertices = [] for v in vertices: if v not in unique_vertices: unique_vertices.append(v) logger.info(f"📊 去重后 {len(unique_vertices)} 个唯一顶点") if len(unique_vertices) < 3: logger.warning(f"顶点数量不足,无法创建面: {len(unique_vertices)}") return None # 创建网格 mesh = bpy.data.meshes.new(f"Face_{len(bpy.data.meshes)}") obj = bpy.data.objects.new(f"Face_{len(bpy.data.objects)}", mesh) # 链接到场景 bpy.context.scene.collection.objects.link(obj) # 设置父对象 obj.parent = container # 创建面数据 # 将顶点转换为列表格式 verts = [(v.x, v.y, v.z) for v in unique_vertices] # 创建面的索引(假设是四边形,如果不是则调整) if len(unique_vertices) == 4: faces = [(0, 1, 2, 3)] logger.info("📐 创建四边形面") elif len(unique_vertices) == 3: faces = [(0, 1, 2)] logger.info("📐 创建三角形面") else: # 对于更多顶点,创建三角形面 faces = [] for i in range(1, len(unique_vertices) - 1): faces.append((0, i, i + 1)) logger.info(f"📐 创建 {len(faces)} 个三角形面") # 创建网格数据 mesh.from_pydata(verts, [], faces) mesh.update() # 设置面的属性 obj["sw_p"] = surface.get("p", 0) obj["sw_f"] = surface.get("f", 0) # 【新增】为Zone的面添加透明材质 - 参考suw_impl.py的实现 try: if obj.data: # 创建透明材质名称 material_name = "Zone_Transparent" # 检查是否已存在 if material_name in bpy.data.materials: transparent_material = bpy.data.materials[material_name] else: # 创建新的透明材质 transparent_material = bpy.data.materials.new( name=material_name) transparent_material.use_nodes = True # 设置透明属性 if transparent_material.node_tree: principled = transparent_material.node_tree.nodes.get( "Principled BSDF") if principled: # 设置基础颜色为半透明白色 principled.inputs['Base Color'].default_value = ( 1.0, 1.0, 1.0, 0.5) # 设置Alpha为完全透明 principled.inputs['Alpha'].default_value = 0.0 # 设置混合模式 transparent_material.blend_method = 'BLEND' # 清空现有材质 obj.data.materials.clear() # 添加透明材质 obj.data.materials.append(transparent_material) logger.info(f"✅ 为Zone面 {obj.name} 添加透明材质") else: logger.warning(f"无法为Zone面 {obj.name} 添加透明材质:缺少网格数据") except Exception as material_error: logger.error(f"为Zone面添加透明材质失败: {material_error}") logger.info(f"✅ 面创建成功: {obj.name}, 顶点数: {len(unique_vertices)}") return obj except Exception as e: logger.error(f"创建面失败: {e}") import traceback logger.error(traceback.format_exc()) return None # ==================== 核心删除方法 ==================== def _del_unit_complete(self, uid: str): """完整删除单元 - 对应c03/c04的完整创建逻辑""" try: logger.info(f"🗑️ 开始完整删除单元: {uid}") # 1. 删除所有区域 (对应c03创建的zones) if hasattr(self.data_manager, 'zones') and uid in self.data_manager.zones: zones_to_delete = list(self.data_manager.zones[uid].keys()) for zid in zones_to_delete: self._del_zone_complete(uid, zid) # 清空zones字典 del self.data_manager.zones[uid] logger.info(f"✅ 清理了单元 {uid} 的所有区域数据") # 2. 删除所有部件 (对应c04创建的parts) if hasattr(self.data_manager, 'parts') and uid in self.data_manager.parts: parts_to_delete = list(self.data_manager.parts[uid].keys()) for cp in parts_to_delete: self._del_part_complete(uid, cp) # 清空parts字典 del self.data_manager.parts[uid] logger.info(f"✅ 清理了单元 {uid} 的所有部件数据") # 3. 删除所有硬件 (对应c08创建的hardwares) if hasattr(self.data_manager, 'hardwares') and uid in self.data_manager.hardwares: hardwares_to_delete = list( self.data_manager.hardwares[uid].keys()) for hw_id in hardwares_to_delete: self._del_hardware_complete(uid, hw_id) # 清空hardwares字典 del self.data_manager.hardwares[uid] logger.info(f"✅ 清理了单元 {uid} 的所有硬件数据") # 4. 删除所有加工 (对应c05创建的machinings) if hasattr(self.data_manager, 'machinings') and uid in self.data_manager.machinings: del self.data_manager.machinings[uid] logger.info(f"✅ 清理了单元 {uid} 的所有加工数据") # 5. 删除所有尺寸标注 (对应c07创建的dimensions) if hasattr(self.data_manager, 'dimensions') and uid in self.data_manager.dimensions: del self.data_manager.dimensions[uid] logger.info(f"✅ 清理了单元 {uid} 的所有尺寸标注数据") # 6. 清理单元级别的数据 self._cleanup_unit_data(uid) # 7. 清理c15缓存 if hasattr(self.data_manager, '_clear_c15_cache'): self.data_manager._clear_c15_cache(uid) # 更新统计 self.deletion_stats["units_deleted"] += 1 logger.info(f"✅ 单元 {uid} 完整删除完成") return 1 # 返回1表示成功 except Exception as e: logger.error(f"完整删除单元失败 {uid}: {e}") self.deletion_stats["deletion_errors"] += 1 return None # 返回None表示失败 def _delete_hierarchy(self, root_obj) -> int: """ [V4 Helper] Deletes a root object and its entire hierarchy of children using BFS and a reversed list. Returns the number of objects deleted. """ if not self._is_object_valid(root_obj): return 0 # 1. Collect all objects in the hierarchy using Breadth-First Search all_objects_in_hierarchy = [] queue = [root_obj] visited_in_hierarchy = {root_obj} while queue: current_obj = queue.pop(0) all_objects_in_hierarchy.append(current_obj) for child in current_obj.children: if child not in visited_in_hierarchy: visited_in_hierarchy.add(child) queue.append(child) # 2. Delete objects in reverse order (children first) deleted_count = 0 for obj_to_delete in reversed(all_objects_in_hierarchy): if self._delete_object_safe(obj_to_delete): deleted_count += 1 return deleted_count def _del_zone_complete(self, uid: str, zid: int): """ [V4] 完整删除区域及其所有后代对象(Parts, Boards, Faces等)。 """ try: logger.info(f"🗑️ [V3] 开始删除区域及其所有后代: uid={uid}, zid={zid}") if hasattr(bpy.app, 'is_job_running'): try: if bpy.app.is_job_running('RENDER') or bpy.app.is_job_running('OBJECT_BAKE'): logger.error("删除操作必须在主线程中执行") return 0 except Exception: pass total_deleted_count = 0 zone_objects = [ obj for obj in bpy.data.objects if (obj.get("sw_uid") == uid and obj.get("sw_zid") == zid and obj.get("sw_typ") == "zone") or (obj.name == f"Zone_{uid}" and obj.get("sw_zid") == zid) ] if not zone_objects: logger.warning(f"⚠️ 未找到匹配的Zone对象: uid={uid}, zid={zid}") if (self.data_manager and hasattr(self.data_manager, 'zones') and uid in self.data_manager.zones and zid in self.data_manager.zones[uid]): del self.data_manager.zones[uid][zid] logger.info(f"✅ 从数据结构中移除了不存在的Zone记录") return 0 for zone_obj in zone_objects: deleted_count = self._delete_hierarchy(zone_obj) logger.info( f"✅ 区域删除完成: 共删除了 {deleted_count} 个对象 (uid={uid}, zid={zid})") self.deletion_stats["objects_deleted"] += deleted_count return deleted_count except Exception as e: logger.error(f"❌ 删除区域时发生严重错误: {e}") self.deletion_stats["deletion_errors"] += 1 import traceback logger.error(traceback.format_exc()) return 0 def _del_part_complete(self, uid: str, cp: int): """完整删除部件 - [V2] 使用层级删除""" try: part_obj = self.data_manager.get_parts({"uid": uid}).get(cp) if not self._is_object_valid(part_obj): logger.warning(f"部件无效或已被删除,跳过: cp={cp}") # 即使对象无效,也应该从数据管理器中移除记录 self.data_manager.remove_part(uid, cp) return 0 deleted_count = self._delete_hierarchy(part_obj) self.data_manager.remove_part(uid, cp) logger.info(f"✅ 成功删除部件及其子对象: cp={cp}, 删除数量={deleted_count}") return deleted_count except Exception as e: logger.error(f"❌ 删除部件时发生严重错误: {e}") self.deletion_stats["deletion_errors"] += 1 return 0 def _del_hardware_complete(self, uid: str, hw_id: int): """完整删除硬件 - [V2] 使用层级删除""" try: hw_obj = self.data_manager.get_hardwares({"uid": uid}).get(hw_id) if not self._is_object_valid(hw_obj): logger.warning(f"硬件无效或已被删除,跳过: hw_id={hw_id}") self.data_manager.remove_hardware(uid, hw_id) return 0 deleted_count = self._delete_hierarchy(hw_obj) self.data_manager.remove_hardware(uid, hw_id) logger.info(f"✅ 成功删除硬件及其子对象: hw_id={hw_id}, 删除数量={deleted_count}") return deleted_count except Exception as e: logger.error(f"❌ 删除硬件时发生严重错误: {e}") self.deletion_stats["deletion_errors"] += 1 return 0 # ==================== 辅助方法 ==================== def _is_object_valid(self, obj) -> bool: """检查对象是否有效 - 改进版本""" try: if not obj: return False if not BLENDER_AVAILABLE: return True # 【修复】更全面的有效性检查 if not hasattr(obj, 'name'): return False # 检查对象是否在Blender数据中 if obj.name not in bpy.data.objects: return False # 检查对象是否已被标记为删除 if hasattr(obj, 'is_updated_data') and obj.is_updated_data: return False return True except Exception as e: logger.debug(f"检查对象有效性时发生错误: {e}") return False def _delete_object_safe(self, obj) -> bool: """安全删除对象 - 修复版本,添加主线程检查""" try: if not obj or not BLENDER_AVAILABLE: return False # 【修复1】检查是否在主线程中 - 修复is_job_running调用 if hasattr(bpy.app, 'is_job_running'): try: # 检查是否有任何后台任务在运行 if bpy.app.is_job_running('RENDER') or bpy.app.is_job_running('OBJECT_BAKE'): logger.warning("对象删除操作必须在主线程中执行") return False except Exception: # 如果检查失败,继续执行 pass # 【修复2】检查对象是否仍然有效 if not self._is_object_valid(obj): logger.debug( f"对象已无效,跳过删除: {obj.name if hasattr(obj, 'name') else 'unknown'}") return True # 对象已经不存在,视为删除成功 # 【修复3】检查对象是否在Blender数据中 if hasattr(obj, 'name'): if obj.name not in bpy.data.objects: logger.debug(f"对象不在bpy.data.objects中,跳过删除: {obj.name}") return True # 对象已经不在数据中,视为删除成功 # 【修复4】安全删除对象 - 添加更多错误处理 try: # 在删除前记录对象名称 obj_name = obj.name if hasattr(obj, 'name') else 'unknown' # 检查对象是否仍然有效 if not self._is_object_valid(obj): logger.debug(f"对象在删除前已无效: {obj_name}") return True # 执行删除 bpy.data.objects.remove(obj, do_unlink=True) logger.debug(f"✅ 成功删除对象: {obj_name}") return True except Exception as e: # 检查是否是"StructRNA has been removed"错误 if "StructRNA" in str(e) and "removed" in str(e): logger.debug( f"对象已被移除: {obj.name if hasattr(obj, 'name') else 'unknown'}") return True # 对象已被移除,视为删除成功 else: logger.error( f"删除对象失败: {obj.name if hasattr(obj, 'name') else 'unknown'}, 错误: {e}") return False except Exception as e: # 检查是否是"StructRNA has been removed"错误 if "StructRNA" in str(e) and "removed" in str(e): logger.debug( f"对象已被移除: {obj.name if hasattr(obj, 'name') else 'unknown'}") return True # 对象已被移除,视为删除成功 else: logger.error(f"安全删除对象时发生错误: {e}") return False def _cleanup_orphaned_meshes(self): """清理孤立的网格数据 - 改进版本""" try: # 【修复】更安全的清理逻辑 meshes_to_remove = [] for mesh in bpy.data.meshes: try: if mesh.users == 0: meshes_to_remove.append(mesh) except Exception as e: # 检查是否是"StructRNA has been removed"错误 if "StructRNA" in str(e) and "removed" in str(e): logger.debug(f"网格已被移除,跳过清理") continue else: logger.debug(f"检查网格时发生错误: {e}") continue for mesh in meshes_to_remove: try: # 再次检查网格是否仍然有效 if not self._is_object_valid(mesh): logger.debug(f"网格已无效,跳过删除") continue bpy.data.meshes.remove(mesh) logger.debug(f"清理孤立网格: {mesh.name}") except Exception as e: # 检查是否是"StructRNA has been removed"错误 if "StructRNA" in str(e) and "removed" in str(e): logger.debug(f"网格已被移除,跳过删除") continue else: logger.debug(f"删除孤立网格失败: {e}") # 【修复】更安全的材质清理逻辑 materials_to_remove = [] for material in bpy.data.materials: try: # 【修复】检查材质是否仍然有效 if not material or not hasattr(material, 'users'): continue if material.users == 0: materials_to_remove.append(material) except Exception as e: # 检查是否是"StructRNA has been removed"错误 if "StructRNA" in str(e) and "removed" in str(e): logger.debug(f"材质已被移除,跳过清理") continue else: logger.debug(f"检查材质时发生错误: {e}") continue for material in materials_to_remove: try: # 【修复】再次检查材质是否仍然有效 if material and hasattr(material, 'name') and material.name in bpy.data.materials: bpy.data.materials.remove(material) logger.debug(f"清理孤立材质: {material.name}") except Exception as e: # 检查是否是"StructRNA has been removed"错误 if "StructRNA" in str(e) and "removed" in str(e): logger.debug(f"材质已被移除,跳过删除") continue else: logger.debug(f"删除孤立材质失败: {e}") except Exception as e: logger.debug(f"清理孤立数据时发生错误: {e}") def _cleanup_unit_data(self, uid: str): """清理单元数据""" try: logger.info(f"🧹 清理单元数据: {uid}") # 清理单元变换数据 if hasattr(self.data_manager, 'unit_trans') and uid in self.data_manager.unit_trans: del self.data_manager.unit_trans[uid] logger.info(f"✅ 清理单元变换数据: {uid}") # 清理单元参数数据 if hasattr(self.data_manager, 'unit_param') and uid in self.data_manager.unit_param: del self.data_manager.unit_param[uid] logger.info(f"✅ 清理单元参数数据: {uid}") # 强制垃圾回收 import gc gc.collect() except Exception as e: logger.error(f"清理单元数据失败: {e}") def _clear_labels_safe(self, uid: str = None): """安全清理标签 - 修复参数问题""" try: if not BLENDER_AVAILABLE: return # 查找并删除标签对象 labels_to_delete = [] for obj in bpy.data.objects: if obj.get("sw_typ") == "label" or "Label" in obj.name: # 如果指定了uid,只删除该uid的标签 if uid is None or obj.get("sw_uid") == uid: labels_to_delete.append(obj) for label in labels_to_delete: self._delete_object_safe(label) if labels_to_delete: logger.info(f"✅ 清理了 {len(labels_to_delete)} 个标签对象") except Exception as e: logger.error(f"清理标签失败: {e}") # ==================== 统计和管理方法 ==================== def get_deletion_stats(self) -> Dict[str, Any]: """获取删除统计信息""" try: return { "deletion_stats": self.deletion_stats.copy(), "total_deletions": ( self.deletion_stats["units_deleted"] + self.deletion_stats["zones_deleted"] + self.deletion_stats["parts_deleted"] + self.deletion_stats["hardwares_deleted"] ), "success_rate": ( 1.0 - (self.deletion_stats["deletion_errors"] / max(1, sum(self.deletion_stats.values()))) ) * 100 } except Exception as e: logger.error(f"获取删除统计失败: {e}") return {"error": str(e)} def reset_deletion_stats(self): """重置删除统计""" self.deletion_stats = { "units_deleted": 0, "zones_deleted": 0, "parts_deleted": 0, "hardwares_deleted": 0, "objects_deleted": 0, "deletion_errors": 0 } logger.info("删除统计已重置") def cleanup(self): """清理删除管理器""" try: # 重置统计 self.reset_deletion_stats() logger.info("✅ 删除管理器清理完成") except Exception as e: logger.error(f"清理删除管理器失败: {e}") def _clear_selection(self): """清除所有选择 - 对应Ruby的sel_clear""" try: if not BLENDER_AVAILABLE: return # 清除所有对象的选择状态 for obj in bpy.data.objects: try: if hasattr(obj, 'select_set'): obj.select_set(False) except Exception: pass # 清除活动对象 if hasattr(bpy.context, 'view_layer'): bpy.context.view_layer.objects.active = None logger.debug("✅ 清除所有选择完成") except Exception as e: logger.error(f"清除选择失败: {e}") def _del_entities_by_type(self, entities: Dict[str, Any], typ: str, oid: int, uid: str = None) -> int: """按类型删除实体 - V4: 使用层级删除""" try: if not entities: return 0 total_deleted_count = 0 # 创建一个要检查的键的副本,因为字典会在循环中被修改 keys_to_check = list(entities.keys()) for key in keys_to_check: # 检查键是否仍然存在,因为它可能作为另一个实体的子级被删除了 if key not in entities: continue entity = entities[key] if self._is_object_valid(entity): if typ == "uid" or self._get_entity_attribute(entity, typ) == oid: # 执行层级删除 deleted_in_hierarchy = self._delete_hierarchy(entity) total_deleted_count += deleted_in_hierarchy # 删除后,从原始字典中移除键 if key in entities: del entities[key] logger.info( f"✅ 按类型删除实体: typ={typ}, oid={oid}, 删除数量={total_deleted_count}") return total_deleted_count except Exception as e: logger.error(f"❌ 按类型删除实体失败: typ={typ}, oid={oid}, 错误: {e}") return 0 def _get_entity_attribute(self, entity, attr_name: str): """获取实体属性 - 对应Ruby的get_attribute逻辑""" try: if BLENDER_AVAILABLE and hasattr(entity, 'get'): # 检查多种可能的属性名 possible_attrs = [ f"sw_{attr_name}", attr_name, f"sw{attr_name}" ] for attr in possible_attrs: value = entity.get(attr) if value is not None: return value return None except: return None def _cleanup_machinings(self, uid: str): """清理加工数据 - 对应Ruby的machinings清理逻辑""" try: if not self.data_manager or not hasattr(self.data_manager, 'machinings'): return if uid in self.data_manager.machinings: machinings = self.data_manager.machinings[uid] # 清理已删除的加工对象 (对应Ruby的delete_if{|entity| entity.deleted?}) valid_machinings = [] for machining in machinings: if machining and self._is_object_valid(machining): valid_machinings.append(machining) self.data_manager.machinings[uid] = valid_machinings logger.info( f"✅ 清理加工数据: uid={uid}, 保留{len(valid_machinings)}个有效加工") except Exception as e: logger.error(f"清理加工数据失败: {e}") def _del_dimensions(self, data: Dict[str, Any]): """删除尺寸标注 - 对应Ruby的c0c方法""" try: uid = data.get("uid") if not uid or not self.data_manager or not hasattr(self.data_manager, 'dimensions'): return if uid in self.data_manager.dimensions: dimensions = self.data_manager.dimensions[uid] deleted_count = 0 # 删除所有尺寸标注 for dim in dimensions: if dim and self._is_object_valid(dim): if self._delete_object_safe(dim): deleted_count += 1 # 从数据结构中移除 del self.data_manager.dimensions[uid] logger.info(f"✅ 删除尺寸标注: uid={uid}, 删除了{deleted_count}个标注") except Exception as e: logger.error(f"删除尺寸标注失败: {e}") def _clear_door_labels_safe(self, uid: str = None): """安全清理门标签 - 修复参数问题""" try: if not BLENDER_AVAILABLE or not self.data_manager: return if hasattr(self.data_manager, 'door_labels') and self.data_manager.door_labels: # 查找并删除门标签对象 door_labels_to_delete = [] for obj in bpy.data.objects: if (obj.get("sw_typ") == "door_label" or "DoorLabel" in obj.name or obj.get("sw_label_type") == "door"): # 如果指定了uid,只删除该uid的门标签 if uid is None or obj.get("sw_uid") == uid: door_labels_to_delete.append(obj) for label in door_labels_to_delete: self._delete_object_safe(label) if door_labels_to_delete: logger.info(f"✅ 清理了 {len(door_labels_to_delete)} 个门标签对象") except Exception as e: logger.error(f"清理门标签失败: {e}") def _matches_delete_condition(self, entity, typ: str, oid: int, uid: str = None) -> bool: """检查实体是否匹配删除条件 - 添加详细调试""" try: if not entity or not hasattr(entity, 'get'): return False # 【调试】打印实体的所有属性 entity_uid = entity.get("sw_uid") entity_typ_value = entity.get(f"sw_{typ}") logger.debug( f"🔍 检查删除条件: {entity.name if hasattr(entity, 'name') else 'unknown'}") logger.debug( f" 实体属性: sw_uid={entity_uid}, sw_{typ}={entity_typ_value}") logger.debug(f" 删除条件: uid={uid}, typ={typ}, oid={oid}") # 【修复】正确的删除条件逻辑 if typ == "uid": # 删除整个单元:检查sw_uid uid_matches = entity_uid == oid logger.debug(f" uid删除匹配: {uid_matches}") return uid_matches else: # 删除特定类型:需要同时匹配uid和对应的类型属性 uid_matches = uid is None or entity_uid == uid typ_matches = entity_typ_value == oid logger.debug( f" 类型删除匹配: uid匹配={uid_matches}, {typ}匹配={typ_matches}") # 必须同时匹配uid和类型值 return uid_matches and typ_matches except Exception as e: logger.error(f"检查删除条件时发生错误: {e}") return False def _delete_blender_objects_by_type(self, typ: str, oid: int, uid: str = None) -> int: """从Blender中删除指定类型的对象 - 添加详细调试""" deleted_count = 0 try: logger.info(f" 开始搜索Blender对象: typ={typ}, oid={oid}, uid={uid}") # 遍历所有Blender对象 objects_to_delete = [] checked_objects = [] for obj in bpy.data.objects: checked_objects.append(obj.name) if self._should_delete_blender_object(obj, typ, oid, uid): objects_to_delete.append(obj) logger.info(f"🎯 标记删除: {obj.name}") logger.info( f"📊 检查了 {len(checked_objects)} 个对象,标记删除 {len(objects_to_delete)} 个") # 删除收集到的对象 for obj in objects_to_delete: try: logger.info( f"️ 删除Blender对象: {obj.name}, typ={typ}, oid={oid}, uid={uid}") bpy.data.objects.remove(obj, do_unlink=True) deleted_count += 1 except Exception as e: logger.error(f"删除Blender对象失败: {obj.name}, 错误: {e}") # 清理孤立的网格数据 注释 # self._cleanup_orphaned_meshes() except Exception as e: logger.error(f"删除Blender对象时发生错误: {e}") return deleted_count def _should_delete_blender_object(self, obj, typ: str, oid: int, uid: str = None) -> bool: """判断是否应该删除Blender对象 - 添加详细调试""" try: if not obj or not hasattr(obj, 'get'): return False # 【调试】打印对象的所有sw_属性 sw_attrs = {} for key, value in obj.items(): if key.startswith('sw_'): sw_attrs[key] = value logger.debug(f"🔍 检查对象 {obj.name} 的sw_属性: {sw_attrs}") # 使用相同的删除条件逻辑 should_delete = self._matches_delete_condition(obj, typ, oid, uid) if should_delete: logger.info(f"✅ 对象 {obj.name} 匹配删除条件") else: logger.debug(f"❌ 对象 {obj.name} 不匹配删除条件") return should_delete except Exception as e: logger.error(f"检查Blender对象删除条件时发生错误: {e}") return False # ==================== 全局删除管理器实例 ==================== # 全局删除管理器实例 deletion_manager: Optional[DeletionManager] = None def init_deletion_manager() -> DeletionManager: """初始化全局删除管理器实例""" global deletion_manager if deletion_manager is None: deletion_manager = DeletionManager() return deletion_manager def get_deletion_manager() -> DeletionManager: """获取全局删除管理器实例""" global deletion_manager if deletion_manager is None: deletion_manager = init_deletion_manager() return deletion_manager # 自动初始化 deletion_manager = init_deletion_manager()