blenderpython/suw_core/deletion_manager.py

1225 lines
48 KiB
Python
Raw Normal View History

2025-08-01 17:13:30 +08:00
#!/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()