suwoodblender/blenderpython/suw_impl.py1

7117 lines
268 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SUW Implementation - Python翻译版本
原文件: SUWImpl.rb (2019行)
用途: 核心实现类SUWood的主要功能
翻译进度: 完整实现 - 对应Ruby版本所有功能
内存管理优化: 应用 Blender Python API 最佳实践
"""
import re
import math
import logging
import time
import gc
import weakref
import threading
import queue
from typing import Optional, Any, Dict, List, Tuple, Union, Callable
from contextlib import contextmanager
# 设置日志
logger = logging.getLogger(__name__)
# 尝试相对导入,失败则使用绝对导入
try:
from .suw_constants import SUWood
except ImportError:
try:
from suw_constants import SUWood
except ImportError:
# 如果都找不到,创建一个基本的存根
class SUWood:
@staticmethod
def suwood_path(version):
return "."
try:
import bpy
import mathutils
import bmesh
BLENDER_AVAILABLE = True
except ImportError:
BLENDER_AVAILABLE = False
print("⚠️ Blender API 不可用,使用基础几何类")
# 创建存根mathutils模块
class MockMathutils:
class Vector:
def __init__(self, vec):
self.x, self.y, self.z = vec[:3] if len(
vec) >= 3 else (vec + [0, 0])[:3]
def normalized(self):
return self
def dot(self, other):
return 0
class Matrix:
@staticmethod
def Scale(scale, size, axis):
return MockMathutils.Matrix()
@staticmethod
def Translation(vec):
return MockMathutils.Matrix()
@staticmethod
def Rotation(angle, size):
return MockMathutils.Matrix()
def __matmul__(self, other):
return MockMathutils.Matrix()
mathutils = MockMathutils()
# ==================== 内存管理核心类 ====================
class BlenderMemoryManager:
"""Blender内存管理器 - 修复弱引用问题"""
def __init__(self):
# 改用普通集合和字典来跟踪对象,而不是弱引用
self.tracked_objects = set() # 存储对象名称而不是对象本身
self.tracked_meshes = set() # 存储网格名称
self.tracked_images = set() # 存储图像名称
self.tracked_materials = set() # 存储材质名称
self.tracked_collections = set() # 存储集合名称
self.cleanup_interval = 100
self.operation_count = 0
self.last_cleanup = time.time()
self.max_memory_mb = 2048
self._cleanup_lock = threading.Lock()
def register_object(self, obj):
"""注册对象到内存管理器 - 修复版本"""
if obj is None or not BLENDER_AVAILABLE:
return
try:
with self._cleanup_lock:
# 根据对象类型分别处理
if hasattr(obj, 'name'):
obj_name = obj.name
# 根据对象类型存储到不同的集合
if hasattr(obj, 'type'): # Blender Object
self.tracked_objects.add(obj_name)
elif str(type(obj)).find('Material') != -1: # Material
self.tracked_materials.add(obj_name)
elif str(type(obj)).find('Mesh') != -1: # Mesh
self.tracked_meshes.add(obj_name)
elif str(type(obj)).find('Image') != -1: # Image
self.tracked_images.add(obj_name)
elif str(type(obj)).find('Collection') != -1: # Collection
self.tracked_collections.add(obj_name)
else:
self.tracked_objects.add(obj_name)
self.operation_count += 1
# 定期清理
if self.should_cleanup():
self.cleanup_orphaned_data()
except Exception as e:
# 静默处理,不输出错误日志
pass
def register_mesh(self, mesh):
"""注册网格到内存管理器 - 修复版本"""
if mesh is None or not BLENDER_AVAILABLE:
return
try:
with self._cleanup_lock:
if hasattr(mesh, 'name'):
self.tracked_meshes.add(mesh.name)
self.operation_count += 1
except Exception as e:
# 静默处理
pass
def register_image(self, image):
"""注册图像到内存管理器 - 修复版本"""
if image is None or not BLENDER_AVAILABLE:
return
try:
with self._cleanup_lock:
if hasattr(image, 'name'):
self.tracked_images.add(image.name)
self.operation_count += 1
except Exception as e:
# 静默处理
pass
def should_cleanup(self):
"""检查是否需要清理"""
return (self.operation_count >= self.cleanup_interval or
time.time() - self.last_cleanup > 300) # 5分钟强制清理
def cleanup_orphaned_data(self):
"""【暂时禁用】清理孤立的数据块 - 让Blender自动处理以避免冲突"""
if not BLENDER_AVAILABLE:
return
# 【临时策略】完全禁用自动清理,只更新跟踪状态
logger.debug("🚫 自动清理已禁用让Blender自动处理孤立数据")
try:
with self._cleanup_lock:
# 只清理跟踪列表,不实际删除任何数据
invalid_objects = []
invalid_meshes = []
invalid_materials = []
invalid_images = []
# 清理无效的对象引用(不删除实际对象)
for obj_name in list(self.tracked_objects):
try:
if obj_name not in bpy.data.objects:
invalid_objects.append(obj_name)
except:
invalid_objects.append(obj_name)
# 清理无效的网格引用(不删除实际网格)
for mesh_name in list(self.tracked_meshes):
try:
if mesh_name not in bpy.data.meshes:
invalid_meshes.append(mesh_name)
except:
invalid_meshes.append(mesh_name)
# 清理无效的材质引用(不删除实际材质)
for mat_name in list(self.tracked_materials):
try:
if mat_name not in bpy.data.materials:
invalid_materials.append(mat_name)
except:
invalid_materials.append(mat_name)
# 清理无效的图像引用(不删除实际图像)
for img_name in list(self.tracked_images):
try:
if img_name not in bpy.data.images:
invalid_images.append(img_name)
except:
invalid_images.append(img_name)
# 只更新跟踪列表,不删除实际数据
for obj_name in invalid_objects:
self.tracked_objects.discard(obj_name)
for mesh_name in invalid_meshes:
self.tracked_meshes.discard(mesh_name)
for mat_name in invalid_materials:
self.tracked_materials.discard(mat_name)
for img_name in invalid_images:
self.tracked_images.discard(img_name)
total_cleaned = len(invalid_objects) + len(invalid_meshes) + len(invalid_materials) + len(invalid_images)
if total_cleaned > 0:
logger.debug(f"🧹 清理了 {total_cleaned} 个无效引用(不删除实际数据)")
# 【修复】安全清理材质数据
materials_to_remove = []
for material_name in list(self.tracked_materials):
try:
if material_name in bpy.data.materials:
material = bpy.data.materials[material_name]
if material.users == 0:
materials_to_remove.append(material_name)
else:
self.tracked_materials.discard(material_name)
except Exception as e:
logger.warning(f"检查材质 {material_name} 时出错: {e}")
self.tracked_materials.discard(material_name)
# 批量删除无用的材质
for material_name in materials_to_remove:
try:
if material_name in bpy.data.materials:
material = bpy.data.materials[material_name]
bpy.data.materials.remove(material, do_unlink=True)
cleanup_count += 1
self.tracked_materials.discard(material_name)
except Exception as e:
logger.warning(f"删除材质数据失败: {e}")
self.tracked_materials.discard(material_name)
# 【修复】安全清理图像数据
images_to_remove = []
for image_name in list(self.tracked_images):
try:
if image_name in bpy.data.images:
image = bpy.data.images[image_name]
if image.users == 0:
images_to_remove.append(image_name)
else:
self.tracked_images.discard(image_name)
except Exception as e:
logger.warning(f"检查图像 {image_name} 时出错: {e}")
self.tracked_images.discard(image_name)
# 批量删除无用的图像
for image_name in images_to_remove:
try:
if image_name in bpy.data.images:
image = bpy.data.images[image_name]
bpy.data.images.remove(image, do_unlink=True)
cleanup_count += 1
self.tracked_images.discard(image_name)
except Exception as e:
logger.warning(f"删除图像数据失败: {e}")
self.tracked_images.discard(image_name)
# 【修复】清理无效的对象引用
invalid_objects = []
for obj_name in list(self.tracked_objects):
try:
if obj_name not in bpy.data.objects:
invalid_objects.append(obj_name)
except Exception as e:
logger.warning(f"检查对象 {obj_name} 时出错: {e}")
invalid_objects.append(obj_name)
for obj_name in invalid_objects:
self.tracked_objects.discard(obj_name)
if cleanup_count > 0:
logger.info(f"🧹 清理了 {cleanup_count} 个孤立数据块")
except Exception as e:
logger.error(f"内存清理过程中发生错误: {e}")
import traceback
traceback.print_exc()
def _cleanup_tracked_references(self):
"""清理跟踪集合中的无效引用"""
try:
# 清理无效的对象引用
valid_objects = set()
for obj_name in self.tracked_objects:
if obj_name in bpy.data.objects:
valid_objects.add(obj_name)
self.tracked_objects = valid_objects
# 清理无效的网格引用
valid_meshes = set()
for mesh_name in self.tracked_meshes:
if mesh_name in bpy.data.meshes:
valid_meshes.add(mesh_name)
self.tracked_meshes = valid_meshes
# 清理无效的材质引用
valid_materials = set()
for mat_name in self.tracked_materials:
if mat_name in bpy.data.materials:
valid_materials.add(mat_name)
self.tracked_materials = valid_materials
# 清理无效的图像引用
valid_images = set()
for img_name in self.tracked_images:
if img_name in bpy.data.images:
valid_images.add(img_name)
self.tracked_images = valid_images
# 清理无效的集合引用
valid_collections = set()
for col_name in self.tracked_collections:
if col_name in bpy.data.collections:
valid_collections.add(col_name)
self.tracked_collections = valid_collections
except Exception as e:
logger.warning(f"清理跟踪引用失败: {e}")
def get_memory_stats(self) -> Dict[str, int]:
"""获取内存统计信息"""
try:
with self._cleanup_lock:
return {
'tracked_objects': len(self.tracked_objects),
'tracked_meshes': len(self.tracked_meshes),
'tracked_materials': len(self.tracked_materials),
'tracked_images': len(self.tracked_images),
'tracked_collections': len(self.tracked_collections),
'operation_count': self.operation_count,
'blender_objects': len(bpy.data.objects) if BLENDER_AVAILABLE else 0,
'blender_meshes': len(bpy.data.meshes) if BLENDER_AVAILABLE else 0,
'blender_materials': len(bpy.data.materials) if BLENDER_AVAILABLE else 0,
'blender_images': len(bpy.data.images) if BLENDER_AVAILABLE else 0,
}
except Exception as e:
logger.error(f"获取内存统计失败: {e}")
return {}
def force_cleanup(self):
"""强制清理"""
try:
with self._cleanup_lock:
self.last_cleanup = 0 # 重置时间以强制清理
self.cleanup_orphaned_data()
except Exception as e:
logger.error(f"强制清理失败: {e}")
# 全局内存管理器实例
memory_manager = BlenderMemoryManager()
# 【新增】依赖图管理器 - 控制更新频率避免冲突
class DependencyGraphManager:
"""依赖图管理器 - 控制更新频率,避免过度更新导致的冲突"""
def __init__(self):
self.last_update_time = 0
self.update_interval = 0.1 # 最小更新间隔(秒)
self.pending_updates = False
self._update_lock = threading.Lock()
def request_update(self, force=False):
"""请求依赖图更新 - 线程安全版本"""
if not BLENDER_AVAILABLE:
return
# 【新增】线程安全检查 - 只在主线程中执行更新
if threading.current_thread().ident != _main_thread_id:
logger.debug("跳过非主线程的依赖图更新")
self.pending_updates = True
return
with self._update_lock:
current_time = time.time()
if force or (current_time - self.last_update_time) >= self.update_interval:
try:
# 【修复】安全的依赖图更新
bpy.context.view_layer.update()
self.last_update_time = current_time
self.pending_updates = False
logger.debug("✅ 依赖图更新完成")
except (AttributeError, ReferenceError, RuntimeError) as e:
# 这些错误在对象删除过程中是预期的
logger.debug(f"依赖图更新时的预期错误: {e}")
except Exception as e:
logger.warning(f"依赖图更新失败: {e}")
else:
self.pending_updates = True
logger.debug("⏳ 依赖图更新被节流控制")
def flush_pending_updates(self):
"""强制执行所有挂起的更新"""
if self.pending_updates:
self.request_update(force=True)
# 全局依赖图管理器
dependency_manager = DependencyGraphManager()
# 全局主线程任务队列
_main_thread_queue = queue.Queue()
_main_thread_id = None
def init_main_thread():
"""初始化主线程ID"""
global _main_thread_id
_main_thread_id = threading.current_thread().ident
def execute_in_main_thread_async(func: Callable, *args, **kwargs):
"""
【真正的异步版】在主线程中安全地调度函数 - 真正的"即发即忘",不等待结果。
"""
global _main_thread_queue, _main_thread_id
# 如果已经在主线程中,直接执行
if threading.current_thread().ident == _main_thread_id:
try:
func(*args, **kwargs)
return True
except Exception as e:
logger.error(f"在主线程直接执行函数时出错: {e}")
import traceback
traceback.print_exc()
return False
# 在Blender中使用应用程序定时器 - 即发即忘模式
try:
import bpy
def timer_task():
try:
func(*args, **kwargs)
except Exception as e:
logger.error(f"主线程任务执行失败: {e}")
import traceback
traceback.print_exc()
return None # 只执行一次
# 注册定时器任务就立即返回,不等待结果
bpy.app.timers.register(timer_task, first_interval=0.001)
# !!!关键:立即返回调度成功,不等待执行结果!!!
return True
except ImportError:
# 不在Blender环境中使用原有的队列机制 - 也改为即发即忘
def wrapper():
try:
func(*args, **kwargs)
except Exception as e:
logger.error(f"队列任务执行失败: {e}")
import traceback
traceback.print_exc()
_main_thread_queue.put(wrapper)
# 立即返回调度成功,不等待执行结果
return True
# 【保持向后兼容】旧函数名的别名
execute_in_main_thread = execute_in_main_thread_async
def process_main_thread_tasks():
"""
【修复版】处理主线程任务队列 - 一次只处理一个任务!
这个函数需要被Blender的定时器定期调用。
"""
global _main_thread_queue
try:
# !!!关键修改:从 while 改为 if
# 一次定时器触发只处理队列中的一个任务然后就把控制权还给Blender。
if not _main_thread_queue.empty():
task = _main_thread_queue.get_nowait()
try:
task()
except Exception as e:
logger.error(f"执行主线程任务时出错: {e}")
import traceback
traceback.print_exc()
except queue.Empty:
pass # 队列是空的,什么也不做
@contextmanager
def safe_blender_operation(operation_name: str):
"""线程安全的Blender操作上下文管理器 - 修复版本"""
if not BLENDER_AVAILABLE:
logger.warning(f"Blender不可用跳过操作: {operation_name}")
yield
return
start_time = time.time()
logger.debug(f"🔄 开始操作: {operation_name}")
# 保存当前状态
original_mode = None
original_selection = []
original_active = None
def _execute_operation():
nonlocal original_mode, original_selection, original_active
try:
# 确保在对象模式下
if hasattr(bpy.context, 'mode') and bpy.context.mode != 'OBJECT':
original_mode = bpy.context.mode
bpy.ops.object.mode_set(mode='OBJECT')
# 保存当前选择和活动对象
if hasattr(bpy.context, 'selected_objects'):
original_selection = list(bpy.context.selected_objects)
if hasattr(bpy.context, 'active_object'):
original_active = bpy.context.active_object
# 清除选择以避免冲突
bpy.ops.object.select_all(action='DESELECT')
return True
except Exception as e:
logger.error(f"准备操作失败: {e}")
return False
def _cleanup_operation():
try:
# 尝试恢复原始状态
bpy.ops.object.select_all(action='DESELECT')
for obj in original_selection:
if obj and obj.name in bpy.data.objects:
obj.select_set(True)
# 恢复活动对象
if original_active and original_active.name in bpy.data.objects:
bpy.context.view_layer.objects.active = original_active
# 恢复模式
if original_mode and original_mode != 'OBJECT':
bpy.ops.object.mode_set(mode=original_mode)
except Exception as restore_error:
logger.warning(f"恢复状态失败: {restore_error}")
try:
# 如果不在主线程,使用主线程执行准备操作
if threading.current_thread().ident != _main_thread_id:
success = execute_in_main_thread(_execute_operation)
if not success:
raise RuntimeError("准备操作失败")
else:
success = _execute_operation()
if not success:
raise RuntimeError("准备操作失败")
# 执行用户操作
yield
elapsed_time = time.time() - start_time
if elapsed_time > 5.0:
logger.warning(f"操作耗时过长: {operation_name} ({elapsed_time:.2f}s)")
else:
logger.debug(f"✅ 操作完成: {operation_name} ({elapsed_time:.2f}s)")
except Exception as e:
logger.error(f"❌ 操作失败: {operation_name} - {e}")
raise
finally:
# 清理操作也需要在主线程中执行
if threading.current_thread().ident != _main_thread_id:
try:
execute_in_main_thread(_cleanup_operation)
except:
pass
else:
_cleanup_operation()
# ==================== 几何类扩展 ====================
class Point3d:
"""3D点类 - 对应Ruby的Geom::Point3d"""
def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0):
self.x = x
self.y = y
self.z = z
@classmethod
def parse(cls, value: str):
"""从字符串解析3D点"""
if not value or value.strip() == "":
return None
# 解析格式: "(x,y,z)" 或 "x,y,z"
clean_value = re.sub(r'[()]*', '', value)
xyz = [float(axis.strip()) for axis in clean_value.split(',')]
# 转换mm为内部单位假设输入是mm
return cls(xyz[0] * 0.001, xyz[1] * 0.001, xyz[2] * 0.001)
def to_s(self, unit: str = "mm", digits: int = -1) -> str:
"""转换为字符串"""
if unit == "cm":
x_val = self.x * 100 # 内部单位转换为cm
y_val = self.y * 100
z_val = self.z * 100
return f"({x_val:.3f}, {y_val:.3f}, {z_val:.3f})"
else: # mm
x_val = self.x * 1000 # 内部单位转换为mm
y_val = self.y * 1000
z_val = self.z * 1000
if digits == -1:
return f"({x_val}, {y_val}, {z_val})"
else:
return f"({x_val:.{digits}f}, {y_val:.{digits}f}, {z_val:.{digits}f})"
def __str__(self):
return self.to_s()
def __repr__(self):
return f"Point3d({self.x}, {self.y}, {self.z})"
class Vector3d:
"""3D向量类 - 对应Ruby的Geom::Vector3d"""
def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0):
self.x = x
self.y = y
self.z = z
@classmethod
def parse(cls, value: str):
"""从字符串解析3D向量"""
if not value or value.strip() == "":
return None
clean_value = re.sub(r'[()]*', '', value)
xyz = [float(axis.strip()) for axis in clean_value.split(',')]
return cls(xyz[0] * 0.001, xyz[1] * 0.001, xyz[2] * 0.001)
def to_s(self, unit: str = "mm") -> str:
"""转换为字符串"""
if unit == "cm":
x_val = self.x * 100 # 内部单位转换为cm
y_val = self.y * 100
z_val = self.z * 100
return f"({x_val:.3f}, {y_val:.3f}, {z_val:.3f})"
elif unit == "in":
return f"({self.x}, {self.y}, {self.z})"
else: # mm
x_val = self.x * 1000 # 内部单位转换为mm
y_val = self.y * 1000
z_val = self.z * 1000
return f"({x_val}, {y_val}, {z_val})"
def normalize(self):
"""归一化向量"""
length = math.sqrt(self.x**2 + self.y**2 + self.z**2)
if length > 0:
return Vector3d(self.x/length, self.y/length, self.z/length)
return Vector3d(0, 0, 0)
def __str__(self):
return self.to_s()
class Transformation:
"""变换矩阵类 - 对应Ruby的Geom::Transformation"""
def __init__(self, origin: Point3d = None, x_axis: Vector3d = None,
y_axis: Vector3d = None, z_axis: Vector3d = None):
self.origin = origin or Point3d(0, 0, 0)
self.x_axis = x_axis or Vector3d(1, 0, 0)
self.y_axis = y_axis or Vector3d(0, 1, 0)
self.z_axis = z_axis or Vector3d(0, 0, 1)
@classmethod
def parse(cls, data: Dict[str, str]):
"""从字典解析变换"""
origin = Point3d.parse(data.get("o"))
x_axis = Vector3d.parse(data.get("x"))
y_axis = Vector3d.parse(data.get("y"))
z_axis = Vector3d.parse(data.get("z"))
return cls(origin, x_axis, y_axis, z_axis)
def store(self, data: Dict[str, str]):
"""存储变换到字典"""
data["o"] = self.origin.to_s("mm")
data["x"] = self.x_axis.to_s("in")
data["y"] = self.y_axis.to_s("in")
data["z"] = self.z_axis.to_s("in")
# ==================== SUWood 材质类型常量 ====================
MAT_TYPE_NORMAL = 0
MAT_TYPE_OBVERSE = 1
MAT_TYPE_NATURE = 2
# ==================== SUWImpl 核心实现类 ====================
class SUWImpl:
"""SUWood核心实现类 - 完整翻译版本,应用内存管理最佳实践"""
_instance = None
_selected_uid = None
_selected_obj = None
_selected_zone = None
_selected_part = None
_scaled_zone = None
_server_path = None
_default_zone = None
_creation_lock = False
_mesh_creation_count = 0
_batch_operation_active = False
def __init__(self):
"""初始化SUWImpl实例"""
# 基础属性
self.zones = {}
self.parts = {}
self.hardwares = {}
self.machinings = {}
self.dimensions = {}
self.textures = {}
self.unit_param = {}
self.unit_trans = {}
# 内存管理相关
self.object_references = {} # 存储对象名称而非引用
self.mesh_cache = {}
self.material_cache = {}
# 批量操作优化
self.deferred_updates = []
self.batch_size = 50
# 状态管理
self.added_contour = False
self.part_mode = False
self.hide_none = False
self.mat_type = MAT_TYPE_NORMAL
self.selected_faces = []
self.selected_parts = []
self.selected_hws = []
self.menu_handle = 0
self.back_material = False
# 图层管理
self.door_layer = None
self.drawer_layer = None
self.labels = None
self.door_labels = None
# 【新增】c15命令优化缓存
self._c15_cache = {
'leaf_zones': {}, # uid -> set of leaf zone ids
'zones_hash': {}, # uid -> hash of zones data
'last_update_time': {}, # uid -> timestamp
'blender_objects_cache': set(), # cached blender object names
'cache_valid_until': 0 # timestamp when cache expires
}
logger.info("SUWImpl 初始化完成启用内存管理优化和c15缓存")
self.command_map = {
# ... existing commands ...
'c16': self._execute_c16, # sel_zone
'c17': self._execute_c17, # sel_elem
# ... existing commands ...
}
@classmethod
def get_instance(cls):
"""获取单例实例"""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def startup(self):
"""启动SUW实现"""
try:
# 初始化主线程
init_main_thread()
logger.info("🚀 启动SUW实现...")
if BLENDER_AVAILABLE:
self._create_layers()
self._init_materials()
self._init_default_zone()
# 启动主线程任务处理器
self._start_main_thread_processor()
logger.info("✅ SUW实现启动完成")
except Exception as e:
logger.error(f"启动SUW实现失败: {e}")
def _start_main_thread_processor(self):
"""启动主线程任务处理器"""
try:
# 使用Blender的定时器来处理主线程任务
if hasattr(bpy.app, 'timers'):
def process_tasks():
process_main_thread_tasks()
return 0.01 # 每10毫秒处理一次
bpy.app.timers.register(process_tasks, persistent=True)
logger.debug("主线程任务处理器已启动")
except Exception as e:
logger.warning(f"启动主线程任务处理器失败: {e}")
def _create_layers(self):
"""创建集合系统在Blender 2.8+中创建集合) - 修复版本"""
try:
if not BLENDER_AVAILABLE:
return
logger.debug("创建集合系统...")
# 创建门板集合(默认可见)
door_collection_name = "DOOR_LAYER"
if door_collection_name not in bpy.data.collections:
door_collection = bpy.data.collections.new(
door_collection_name)
bpy.context.scene.collection.children.link(door_collection)
# 修改:默认显示门板集合
door_collection.hide_viewport = False
door_collection.hide_render = False
logger.debug("门板集合已创建(可见)")
# 创建抽屉集合(默认可见)
drawer_collection_name = "DRAWER_LAYER"
if drawer_collection_name not in bpy.data.collections:
drawer_collection = bpy.data.collections.new(
drawer_collection_name)
bpy.context.scene.collection.children.link(drawer_collection)
# 修改:默认显示抽屉集合
drawer_collection.hide_viewport = False
drawer_collection.hide_render = False
logger.debug("抽屉集合已创建(可见)")
logger.debug("集合系统创建完成")
except Exception as e:
logger.error(f"创建集合系统失败: {e}")
def _init_materials(self):
"""初始化材质 - 减少注册调用"""
try:
if not BLENDER_AVAILABLE:
return
logger.debug("初始化材质...")
# 创建基础材质
materials_to_create = [
("mat_default", (0.8, 0.8, 0.8, 1.0)),
("mat_select", (1.0, 0.5, 0.0, 1.0)),
("mat_normal", (0.7, 0.7, 0.7, 1.0)),
("mat_obverse", (0.9, 0.9, 0.9, 1.0)),
("mat_reverse", (0.6, 0.6, 0.6, 1.0)),
("mat_thin", (0.5, 0.5, 0.5, 1.0)),
]
for mat_name, color in materials_to_create:
if mat_name not in bpy.data.materials:
material = bpy.data.materials.new(name=mat_name)
material.use_nodes = True
# 设置基础颜色
if material.node_tree:
principled = material.node_tree.nodes.get(
"Principled BSDF")
if principled:
principled.inputs['Base Color'].default_value = color
# 只注册一次
memory_manager.register_object(material)
self.textures[mat_name] = material
else:
# 如果材质已存在,直接使用
self.textures[mat_name] = bpy.data.materials[mat_name]
except Exception as e:
logger.error(f"初始化材质失败: {e}")
def _init_default_zone(self):
"""初始化默认区域"""
try:
if not BLENDER_AVAILABLE:
return
logger.debug("初始化默认区域...")
# 创建默认区域模板
self._default_zone = bpy.data.objects.new("DefaultZone", None)
# 不添加到场景中,只作为模板使用
# 设置默认属性
self._default_zone["sw_typ"] = "zid"
self._default_zone.hide_viewport = True
memory_manager.register_object(self._default_zone)
logger.debug("默认区域初始化完成")
except Exception as e:
logger.error(f"初始化默认区域失败: {e}")
# ==================== 材质管理方法 ====================
def add_mat_rgb(self, mat_id: str, alpha: float, r: int, g: int, b: int):
"""添加RGB材质"""
try:
if not BLENDER_AVAILABLE:
return None
# 检查材质是否已存在
if mat_id in self.material_cache:
material_name = self.material_cache[mat_id]
if material_name in bpy.data.materials:
return bpy.data.materials[material_name]
# 创建新材质
material = bpy.data.materials.new(mat_id)
material.use_nodes = True
# 设置颜色
if material.node_tree:
principled = material.node_tree.nodes.get("Principled BSDF")
if principled:
color = (r/255.0, g/255.0, b/255.0, alpha)
principled.inputs[0].default_value = color
# 设置透明度
if alpha < 1.0:
material.blend_method = 'BLEND'
# Alpha input
principled.inputs[21].default_value = alpha
# 缓存材质
self.material_cache[mat_id] = material.name
self.textures[mat_id] = material
memory_manager.register_object(material)
logger.info(f"创建RGB材质: {mat_id}")
return material
except Exception as e:
logger.error(f"创建RGB材质失败: {e}")
return None
def get_texture(self, key: str):
"""获取纹理材质 - 增强版本"""
if not BLENDER_AVAILABLE:
return None
try:
# 检查键是否有效
if not key:
return self.textures.get("mat_default")
# 从缓存中获取
if key in self.textures:
material = self.textures[key]
# 验证材质是否仍然有效
if material and material.name in bpy.data.materials:
return material
else:
# 清理无效的缓存
del self.textures[key]
# 在现有材质中查找
for material in bpy.data.materials:
if key in material.name:
self.textures[key] = material
return material
# 返回默认材质
default_material = self.textures.get("mat_default")
if default_material and default_material.name in bpy.data.materials:
return default_material
logger.warning(f"未找到纹理: {key}")
return None
except Exception as e:
logger.error(f"获取纹理失败: {e}")
return None
# ==================== 数据获取方法 ====================
def get_zones(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""获取区域信息"""
uid = data.get("uid")
if uid not in self.zones:
self.zones[uid] = {}
return self.zones[uid]
def get_parts(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""获取零件信息"""
uid = data.get("uid")
if uid not in self.parts:
self.parts[uid] = {}
return self.parts[uid]
def get_hardwares(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""获取硬件信息"""
uid = data.get("uid")
if uid not in self.hardwares:
self.hardwares[uid] = {}
return self.hardwares[uid]
# ==================== 配置管理方法 ====================
def set_config(self, data: Dict[str, Any]):
"""设置配置"""
try:
if "server_path" in data:
self.__class__._server_path = data["server_path"]
if "order_id" in data:
# 在Blender中存储为场景属性
if BLENDER_AVAILABLE:
bpy.context.scene["sw_order_id"] = data["order_id"]
if "order_code" in data:
if BLENDER_AVAILABLE:
bpy.context.scene["sw_order_code"] = data["order_code"]
if "back_material" in data:
self.back_material = data["back_material"]
if "part_mode" in data:
self.part_mode = data["part_mode"]
if "hide_none" in data:
self.hide_none = data["hide_none"]
if "unit_drawing" in data:
print(
f"{data.get('drawing_name', '')}:\t{data['unit_drawing']}")
if "zone_corner" in data:
zones = self.get_zones(data)
zone = zones.get(data["zid"])
if zone:
zone["sw_cor"] = data["zone_corner"]
# 应用内存管理相关配置
if "memory_cleanup_interval" in data:
memory_manager.cleanup_interval = data["memory_cleanup_interval"]
if "batch_size" in data:
self.batch_size = data["batch_size"]
logger.info(f"设置配置: {len(data)} 个配置项")
except Exception as e:
logger.error(f"设置配置失败: {e}")
# ==================== 材质类型管理方法 ====================
def c11(self, data: Dict[str, Any]):
"""part_obverse - 设置零件正面显示"""
try:
self.mat_type = MAT_TYPE_OBVERSE if data.get(
"v", False) else MAT_TYPE_NORMAL
parts = self.get_parts(data)
for root, part in parts.items():
if part and not self._is_selected_part(part):
self.textured_part(part, False)
logger.info(f"设置零件正面显示: {self.mat_type}")
except Exception as e:
logger.error(f"设置零件正面显示失败: {e}")
def c30(self, data: Dict[str, Any]):
"""part_nature - 设置零件自然显示"""
try:
self.mat_type = MAT_TYPE_NATURE if data.get(
"v", False) else MAT_TYPE_NORMAL
parts = self.get_parts(data)
for root, part in parts.items():
if part and not self._is_selected_part(part):
self.textured_part(part, False)
logger.info(f"设置零件自然显示: {self.mat_type}")
except Exception as e:
logger.error(f"设置零件自然显示失败: {e}")
def _is_selected_part(self, part):
"""检查零件是否被选中"""
return part in self.selected_parts
# ==================== 纹理管理方法 ====================
def c02(self, data: Dict[str, Any]):
"""add_texture - 添加纹理 - 简化版本"""
try:
if not BLENDER_AVAILABLE:
logger.warning("Blender 不可用,跳过纹理创建")
return
ckey = data.get("ckey")
if not ckey:
logger.warning("纹理键为空,跳过创建")
return
# 检查纹理是否已存在且有效
if ckey in self.textures:
existing_material = self.textures[ckey]
if existing_material and existing_material.name in bpy.data.materials:
return existing_material
else:
# 清理无效的缓存
del self.textures[ckey]
def create_material():
try:
# 创建新材质
material = bpy.data.materials.new(name=ckey)
material.use_nodes = True
# 获取材质节点
nodes = material.node_tree.nodes
links = material.node_tree.links
# 清理所有默认节点,重新创建
nodes.clear()
# 创建基础节点
principled = nodes.new(type='ShaderNodeBsdfPrincipled')
principled.location = (0, 0)
output = nodes.new(type='ShaderNodeOutputMaterial')
output.location = (300, 0)
# 连接基础节点
links.new(
principled.outputs['BSDF'], output.inputs['Surface'])
# 设置纹理图像
src_path = data.get("src")
if src_path:
try:
# 安全地加载图像
import os
# 检查路径是否存在
if os.path.exists(src_path):
# 检查是否已经加载过这个图像
image_name = os.path.basename(src_path)
image = bpy.data.images.get(image_name)
if not image:
image = bpy.data.images.load(src_path)
memory_manager.register_image(image)
# 创建纹理坐标节点
tex_coord = nodes.new(
type='ShaderNodeTexCoord')
tex_coord.location = (-600, 0)
# 创建图像纹理节点
tex_image = nodes.new(
type='ShaderNodeTexImage')
tex_image.image = image
tex_image.location = (-300, 0)
# 连接节点
links.new(
tex_coord.outputs['UV'], tex_image.inputs['Vector'])
links.new(
tex_image.outputs['Color'], principled.inputs['Base Color'])
# 如果有透明度也连接Alpha
alpha_value = data.get("alpha", 1.0)
if alpha_value < 1.0:
links.new(
tex_image.outputs['Alpha'], principled.inputs['Alpha'])
material.blend_method = 'BLEND'
material.show_transparent_back = False
else:
# 创建一个纯色材质作为替代
principled.inputs['Base Color'].default_value = (
0.5, 0.5, 0.5, 1.0)
except Exception as img_error:
logger.error(f"加载纹理图像失败: {img_error}")
# 创建纯色材质作为替代
principled.inputs['Base Color'].default_value = (
1.0, 0.0, 0.0, 1.0) # 红色表示错误
else:
# 没有图片路径,创建纯色材质
# 尝试从RGB数据创建颜色
r = data.get("r", 128) / 255.0
g = data.get("g", 128) / 255.0
b = data.get("b", 128) / 255.0
principled.inputs['Base Color'].default_value = (
r, g, b, 1.0)
# 设置透明度
alpha_value = data.get("alpha", 1.0)
principled.inputs['Alpha'].default_value = alpha_value
if alpha_value < 1.0:
material.blend_method = 'BLEND'
material.use_backface_culling = False
# 设置其他属性
if "reflection" in data:
metallic_value = data["reflection"]
principled.inputs['Metallic'].default_value = metallic_value
if "reflection_glossiness" in data:
roughness_value = 1.0 - data["reflection_glossiness"]
principled.inputs['Roughness'].default_value = roughness_value
return material
except Exception as e:
logger.error(f"创建材质失败: {e}")
return None
# 直接执行材质创建(已经在主线程中)
material = create_material()
if material:
# 存储材质
self.textures[ckey] = material
memory_manager.register_object(material)
else:
logger.error(f"材质创建失败: {ckey}")
except Exception as e:
logger.error(f"添加纹理失败 {ckey}: {e}")
# 清理可能创建的无效材质
try:
if ckey in self.textures:
del self.textures[ckey]
if ckey in bpy.data.materials:
bpy.data.materials.remove(bpy.data.materials[ckey])
except:
pass
def c04(self, data: Dict[str, Any]):
"""c04 - 添加部件 - 修复版本"""
try:
if not BLENDER_AVAILABLE:
logger.warning("Blender 不可用,跳过零件创建")
return
uid = data.get("uid")
root = data.get("cp")
if not uid or not root:
logger.error("缺少必要参数: uid或cp")
return
logger.info(f"🔧 开始创建部件: uid={uid}, cp={root}")
def create_part():
try:
# 获取部件集合
parts = self.get_parts(data)
# 检查是否已存在
if root in parts:
existing_part = parts[root]
if existing_part and self._is_object_valid(existing_part):
logger.info(f"✅ 部件 {root} 已存在,跳过创建")
return existing_part
else:
logger.warning(f"清理无效的部件引用: {root}")
del parts[root]
# 创建部件容器
part_name = f"Part_{root}"
part = bpy.data.objects.new(part_name, None)
bpy.context.scene.collection.objects.link(part)
logger.info(f"✅ 创建Part对象: {part_name}")
# 设置部件属性
part["sw_uid"] = uid
part["sw_cp"] = root
part["sw_typ"] = "part"
# 存储部件到数据结构
parts[root] = part
memory_manager.register_object(part)
logger.info(f"✅ 部件存储到数据结构: uid={uid}, cp={root}")
# 处理finals数据
finals = data.get("finals", [])
logger.info(f"📦 处理 {len(finals)} 个板材数据")
created_boards = 0
for i, final_data in enumerate(finals):
try:
board = self._create_board_with_material_and_uv(
part, final_data)
if board:
created_boards += 1
logger.info(f"✅ 板材 {i+1}/{len(finals)} 创建成功: {board.name}")
else:
logger.warning(f"⚠️ 板材 {i+1}/{len(finals)} 创建失败")
except Exception as e:
logger.error(f"❌ 创建板材 {i+1}/{len(finals)} 失败: {e}")
logger.info(f"📊 板材创建统计: {created_boards}/{len(finals)} 成功")
# 验证创建结果
if part.name in bpy.data.objects:
logger.info(f"🎉 部件创建完全成功: {part_name}")
return part
else:
logger.error(f"❌ 部件创建验证失败: {part_name} 不在Blender中")
return None
except Exception as e:
logger.error(f"❌ 创建部件失败: {e}")
import traceback
logger.error(traceback.format_exc())
return None
# 直接执行创建(已经在主线程中)
part = create_part()
if part:
logger.info(f"🎉 c04命令执行成功: uid={uid}, cp={root}, part={part.name}")
return part
else:
logger.error(f"❌ c04命令执行失败: uid={uid}, cp={root}")
return None
except Exception as e:
logger.error(f"❌ c04命令异常: {e}")
import traceback
logger.error(traceback.format_exc())
return None
def _create_board_with_material_and_uv(self, part, data):
"""创建板材并关联材质和启用UV - 恢复立方体算法版本"""
try:
# 获取正反面数据
obv = data.get("obv")
rev = data.get("rev")
if not obv or not rev:
logger.warning("缺少正反面数据,创建默认板材")
return self._create_default_board_with_material(part, data)
# 解析顶点计算精确尺寸
obv_vertices = self._parse_surface_vertices(obv)
rev_vertices = self._parse_surface_vertices(rev)
if len(obv_vertices) >= 3 and len(rev_vertices) >= 3:
# 计算板材的精确边界
all_vertices = obv_vertices + rev_vertices
min_x = min(v[0] for v in all_vertices)
max_x = max(v[0] for v in all_vertices)
min_y = min(v[1] for v in all_vertices)
max_y = max(v[1] for v in all_vertices)
min_z = min(v[2] for v in all_vertices)
max_z = max(v[2] for v in all_vertices)
# 计算中心点和精确尺寸
center_x = (min_x + max_x) / 2
center_y = (min_y + max_y) / 2
center_z = (min_z + max_z) / 2
size_x = max(max_x - min_x, 0.001) # 确保最小尺寸
size_y = max(max_y - min_y, 0.001)
size_z = max(max_z - min_z, 0.001)
logger.info(f"🔨 计算板材尺寸: {size_x:.3f}x{size_y:.3f}x{size_z:.3f}m, 中心: ({center_x:.3f},{center_y:.3f},{center_z:.3f})")
# 创建精确尺寸的立方体
bpy.ops.mesh.primitive_cube_add(
size=1,
location=(center_x, center_y, center_z)
)
board = bpy.context.active_object
# 缩放到精确尺寸
board.scale = (size_x, size_y, size_z)
# 设置属性和父子关系
board.parent = part
board.name = f"Board_{part.name}"
board["sw_face_type"] = "board"
board["sw_uid"] = part.get("sw_uid")
board["sw_cp"] = part.get("sw_cp")
board["sw_typ"] = "board"
logger.info(f"✅ 板材属性设置完成: {board.name}, 父对象: {part.name}")
# 关联材质
color = data.get("ckey", "mat_default")
if color:
material = self.get_texture(color)
if material and board.data:
# 清空现有材质
board.data.materials.clear()
# 添加新材质
board.data.materials.append(material)
logger.info(f"✅ 材质 {color} 已关联到板材 {board.name}")
else:
logger.warning(f"材质 {color} 未找到或板材数据无效")
# 启用UV
self._enable_uv_for_board(board)
return board
else:
logger.warning("顶点数据不足,创建默认板材")
return self._create_default_board_with_material(part, data)
except Exception as e:
logger.error(f"创建板材失败: {e}")
return self._create_default_board_with_material(part, data)
def _enable_uv_for_board(self, board):
"""为板件启用UV - 简化版本"""
try:
if not board or not board.data:
logger.warning("无效的板件对象无法启用UV")
return
# 确保网格数据存在
mesh = board.data
if not mesh:
logger.warning("板件没有网格数据")
return
# 创建UV贴图层如果不存在
if not mesh.uv_layers:
uv_layer = mesh.uv_layers.new(name="UVMap")
else:
uv_layer = mesh.uv_layers[0]
# 确保UV层是活动的
mesh.uv_layers.active = uv_layer
# 更新网格数据
mesh.calc_loop_triangles()
# 为立方体创建基本UV坐标
if len(mesh.polygons) == 6: # 标准立方体
# 为每个面分配UV坐标
for poly_idx, poly in enumerate(mesh.polygons):
# 标准UV坐标 (0,0) (1,0) (1,1) (0,1)
uv_coords = [(0.0, 0.0), (1.0, 0.0),
(1.0, 1.0), (0.0, 1.0)]
for loop_idx, loop_index in enumerate(poly.loop_indices):
if loop_idx < len(uv_coords):
uv_layer.data[loop_index].uv = uv_coords[loop_idx]
else:
# 为非标准网格设置简单UV
for loop in mesh.loops:
uv_layer.data[loop.index].uv = (0.5, 0.5)
# 更新网格
mesh.update()
except Exception as e:
logger.error(f"启用UV失败: {e}")
def _create_default_board_with_material(self, part, data):
"""创建默认板材 - 带材质和UV"""
try:
# 创建默认立方体
bpy.ops.mesh.primitive_cube_add(
size=1,
location=(0, 0, 0)
)
board = bpy.context.active_object
# 设置属性和父子关系
board.parent = part
board.name = f"Board_{part.name}_default"
board["sw_face_type"] = "board"
# 从part获取uid和cp信息
uid = part.get("sw_uid")
cp = part.get("sw_cp")
board["sw_uid"] = uid
board["sw_cp"] = cp
board["sw_typ"] = "board"
logger.info(f"✅ 默认板材属性设置完成: {board.name}, 父对象: {part.name}")
# 关联材质
color = data.get("ckey", "mat_default")
if color:
material = self.get_texture(color)
if material and board.data:
board.data.materials.clear()
board.data.materials.append(material)
logger.info(f"✅ 默认材质 {color} 已关联到板件 {board.name}")
# 启用UV
self._enable_uv_for_board(board)
logger.info(f"✅ 创建默认板材: {board.name}")
return board
except Exception as e:
logger.error(f"创建默认板材失败: {e}")
return None
def _add_part_stretch_safe(self, part, data, timeout=5):
"""创建拉伸部件 - 安全版本"""
try:
logger.debug("创建拉伸部件(简化版本)")
# 创建简单的拉伸对象
stretch_obj = bpy.data.objects.new(f"Stretch_{part.name}", None)
stretch_obj.parent = part
bpy.context.scene.collection.objects.link(stretch_obj)
return stretch_obj
except Exception as e:
logger.error(f"创建拉伸部件失败: {e}")
return None
def _add_part_arc_safe(self, part, data, antiz, profiles, timeout=5):
"""创建弧形部件 - 安全版本"""
try:
logger.debug("创建弧形部件(简化版本)")
# 创建简单的弧形对象
arc_obj = bpy.data.objects.new(f"Arc_{part.name}", None)
arc_obj.parent = part
bpy.context.scene.collection.objects.link(arc_obj)
return arc_obj
except Exception as e:
logger.error(f"创建弧形部件失败: {e}")
return None
def _update_viewport(self):
"""更新视图端口"""
try:
if BLENDER_AVAILABLE:
# 更新依赖图
bpy.context.view_layer.update()
# 刷新视图
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
region.tag_redraw()
logger.debug("视图端口已更新")
except Exception as e:
logger.warning(f"更新视图端口失败: {e}")
def _process_final_geometry(self, part, data, virtual=False):
"""处理最终几何体 - 增强版本"""
try:
final = data.get("final")
if not final:
logger.warning("没有找到最终几何体数据")
return
logger.debug(f"处理最终几何体: typ={final.get('typ')}")
# 获取几何体类型
typ = final.get("typ", 1)
antiz = final.get("antiz", False)
profiles = final.get("profiles", {})
# 创建几何体
if typ == 1:
# 板材部件
logger.debug("创建板材部件")
leaf = self._add_part_board(part, final, antiz, profiles)
elif typ == 2:
# 拉伸部件
logger.debug("创建拉伸部件")
leaf = self._add_part_stretch(part, final)
elif typ == 3:
# 弧形部件
logger.debug("创建弧形部件")
leaf = self._add_part_arc(part, final, antiz, profiles)
else:
logger.warning(f"未知的几何体类型: {typ}")
return
if leaf:
# 设置属性
leaf["sw_typ"] = "cp"
leaf["sw_mn"] = final.get("mn", 0)
# 设置可见性
if not virtual:
leaf.hide_viewport = False
memory_manager.register_object(leaf)
logger.debug(f"几何体创建成功: {leaf.name}")
else:
logger.warning("几何体创建失败")
except Exception as e:
logger.error(f"处理最终几何体失败: {e}")
import traceback
logger.error(traceback.format_exc())
def _add_part_board(self, part, data, antiz, profiles):
"""创建板材部件 - 增强版本"""
try:
logger.debug("开始创建板材部件")
# 创建叶子组
leaf = bpy.data.objects.new(f"Board_{part.name}", None)
leaf.parent = part
bpy.context.scene.collection.objects.link(leaf)
# 设置板材属性
leaf["sw_face_type"] = "board"
leaf["sw_uid"] = part.get("sw_uid")
leaf["sw_cp"] = part.get("sw_cp")
leaf["sw_typ"] = "board"
# 获取材质信息
color = data.get("ckey", "mat_default")
scale = data.get("scale")
angle = data.get("angle")
color2 = data.get("ckey2")
scale2 = data.get("scale2")
angle2 = data.get("angle2")
logger.debug(f"板材材质: {color}")
# 处理截面
if "sects" in data:
logger.debug("处理截面数据")
sects = data["sects"]
for sect in sects:
segs = sect.get("segs", [])
surf = sect.get("sect", {})
paths = self._create_paths(part, segs)
self._follow_me(leaf, surf, paths, color, scale, angle)
# 创建第二个叶子用于表面
leaf2 = bpy.data.objects.new(
f"Board_Surface_{part.name}", None)
leaf2.parent = leaf
bpy.context.scene.collection.objects.link(leaf2)
self._add_part_surf(leaf2, data, antiz, color,
scale, angle, color2, scale2, angle2, profiles)
else:
# 直接创建表面
logger.debug("创建板材表面")
self._add_part_surf(
leaf, data, antiz, color, scale, angle, color2, scale2, angle2, profiles)
logger.debug(f"板材部件创建完成: {leaf.name}")
return leaf
except Exception as e:
logger.error(f"创建板材部件失败: {e}")
import traceback
logger.error(traceback.format_exc())
return None
def _add_part_surf(self, leaf, data, antiz, color, scale, angle, color2, scale2, angle2, profiles):
"""创建部件表面 - 增强版本"""
try:
logger.debug("开始创建部件表面")
# 获取正反面数据
obv = data.get("obv") # 正面
rev = data.get("rev") # 反面
if not obv or not rev:
logger.warning("缺少正反面数据")
return
logger.debug(f"正面数据: {obv}")
logger.debug(f"反面数据: {rev}")
# 设置材质类型
obv_type = "o"
obv_save = color
obv_scale = scale
obv_angle = angle
rev_type = "r"
rev_save = color2 if color2 else color
rev_scale = scale2 if color2 else scale
rev_angle = angle2 if color2 else angle
# 如果antiz为真交换正反面
if antiz:
obv_type, rev_type = rev_type, obv_type
obv_save, rev_save = rev_save, obv_save
obv_scale, rev_scale = rev_scale, obv_scale
obv_angle, rev_angle = rev_angle, obv_angle
# 确定显示材质
obv_show = "mat_obverse" if self.mat_type == MAT_TYPE_OBVERSE else obv_save
rev_show = "mat_reverse" if self.mat_type == MAT_TYPE_OBVERSE else rev_save
# 创建面
series1 = []
series2 = []
logger.debug("创建正面")
obv_face = self.create_face(
leaf, obv, obv_show, obv_scale, obv_angle, series1, False, self.back_material, obv_save, obv_type)
logger.debug("创建反面")
rev_face = self.create_face(
leaf, rev, rev_show, rev_scale, rev_angle, series2, True, self.back_material, rev_save, rev_type)
# 创建边缘
if series1 and series2:
logger.debug("创建边缘")
self._add_part_edges(
leaf, series1, series2, obv, rev, profiles)
logger.debug("部件表面创建完成")
except Exception as e:
logger.error(f"创建部件表面失败: {e}")
import traceback
logger.error(traceback.format_exc())
def _clear_part_children(self, part):
"""清除零件的子对象"""
if not BLENDER_AVAILABLE:
return
try:
children_to_remove = []
for child in part.children:
if child.get("sw_typ") == "cp":
children_to_remove.append(child)
for child in children_to_remove:
bpy.data.objects.remove(child, do_unlink=True)
except Exception as e:
logger.error(f"清除零件子对象失败: {e}")
def _set_drawer_properties(self, part, data):
"""设置抽屉属性"""
drawer_type = data.get("drw", 0)
part["sw_drawer"] = drawer_type
if drawer_type in [73, 74]: # DR_LP/DR_RP
part["sw_dr_depth"] = data.get("drd", 0)
if drawer_type == 70: # DR_DP
drv = data.get("drv")
if drv:
drawer_dir = Vector3d.parse(drv)
part["sw_drawer_dir"] = (
drawer_dir.x, drawer_dir.y, drawer_dir.z)
def _set_door_properties(self, part, data):
"""设置门属性"""
door_type = data.get("dor", 0)
part["sw_door"] = door_type
if door_type in [10, 15]:
part["sw_door_width"] = data.get("dow", 0)
part["sw_door_pos"] = data.get("dop", "F")
def _load_prefab_part(self, part, data):
"""加载预制件"""
if "sid" not in data:
return None
try:
mirr = data.get("mr", "")
if mirr:
mirr = "_" + mirr
# 构建文件路径
file_path = f"{SUWood.suwood_path('V_StructPart')}/{data['sid']}{mirr}.skp"
print(f"尝试加载预制件: {file_path}")
# 在Blender中我们需要使用不同的方法加载外部文件
# 这里创建一个占位符
inst = bpy.data.objects.new(f"Prefab_{data['sid']}", None)
inst.parent = part
bpy.context.scene.collection.objects.link(inst)
inst["sw_typ"] = "cp"
# 设置缩放
if "l" in data and "w" in data:
inst.scale = (data["l"] * 0.001, data["w"] * 0.001, 1.0)
# 应用变换
if "trans" in data:
trans = Transformation.parse(data["trans"])
self._apply_transformation(inst, trans)
return inst
except Exception as e:
logger.error(f"加载预制件失败: {e}")
return None
def _create_virtual_geometry(self, part, data):
"""创建虚拟几何体"""
try:
leaf = bpy.data.objects.new("Virtual_Geometry", None)
leaf.parent = part
bpy.context.scene.collection.objects.link(leaf)
if data.get("typ") == 3:
# 弧形部件
self._create_arc_geometry(leaf, data)
else:
# 板材部件
self._create_board_geometry(leaf, data)
leaf["sw_typ"] = "cp"
leaf["sw_virtual"] = True
leaf.hide_viewport = True
except Exception as e:
logger.error(f"创建虚拟几何体失败: {e}")
def _create_arc_geometry(self, leaf, data):
"""创建弧形几何体"""
try:
co = data.get("co")
cr = data.get("cr")
if co and cr:
center_o = Point3d.parse(co)
center_r = Point3d.parse(cr)
# 创建弧形路径
path = self._create_arc_path(leaf, center_o, center_r)
# 创建截面
obv = data.get("obv", {})
self._follow_me(leaf, obv, path, None)
except Exception as e:
logger.error(f"创建弧形几何体失败: {e}")
def _create_board_geometry(self, leaf, data):
"""创建板材几何体"""
try:
obv = data.get("obv", {})
rev = data.get("rev", {})
series1 = []
series2 = []
self.create_face(leaf, obv, series=series1)
self.create_face(leaf, rev, series=series2)
self._add_part_edges(leaf, series1, series2, obv, rev)
except Exception as e:
logger.error(f"创建板材几何体失败: {e}")
def _add_part_edges(self, leaf, series1, series2, obv, rev, profiles=None):
"""创建部件的边缘面"""
try:
if not BLENDER_AVAILABLE or not series1 or not series2:
return
unplanar = False
for index in range(len(series1)):
pts1 = series1[index]
pts2 = series2[index]
for i in range(1, len(pts1)):
pts = [pts1[i-1], pts1[i], pts2[i], pts2[i-1]]
try:
# 创建边缘面
face = self._create_edge_face(leaf, pts)
if face and profiles:
self._add_part_profile(face, index, profiles)
except Exception as e:
unplanar = True
logger.warning(f"Points are not planar {index}: {i}")
logger.warning(f"Points: {pts}")
if unplanar:
# 输出调试信息
segs_o = obv.get("segs", [])
pts_o = [seg[0] for seg in segs_o]
segs_r = rev.get("segs", [])
pts_r = [seg[0] for seg in segs_r]
logger.warning("=" * 30)
logger.warning(f"obv: {pts_o}")
logger.warning(f"rev: {pts_r}")
logger.warning(f"series1: {series1}")
logger.warning(f"series2: {series2}")
logger.warning("=" * 30)
except Exception as e:
logger.error(f"创建部件边缘失败: {e}")
def _create_edge_face(self, container, points):
"""创建边缘面"""
try:
if not BLENDER_AVAILABLE:
return None
# 创建网格
mesh = bpy.data.meshes.new("Edge_Face")
vertices = [(p.x, p.y, p.z) if hasattr(
p, 'x') else p for p in points]
faces = [list(range(len(vertices)))]
mesh.from_pydata(vertices, [], faces)
mesh.update()
# 创建对象
obj = bpy.data.objects.new("Edge_Face_Obj", mesh)
obj.parent = container
bpy.context.scene.collection.objects.link(obj)
# 隐藏某些边
for i, edge in enumerate(mesh.edges):
if i in [1, 3]:
edge.use_edge_sharp = True
return obj
except Exception as e:
logger.error(f"创建边缘面失败: {e}")
return None
def _add_part_profile(self, face, index, profiles):
"""为面添加型材属性和纹理"""
try:
profile = profiles.get(index)
if not profile:
return
color = profile.get("ckey")
scale = profile.get("scale")
angle = profile.get("angle")
typ = profile.get("typ", "0")
# 确定当前颜色
if self.mat_type == MAT_TYPE_OBVERSE:
if typ == "1":
current = "mat_obverse" # thick profile
elif typ == "2":
current = "mat_thin" # thin profile
else:
current = "mat_reverse" # none profile
else:
current = color
face["sw_typ"] = f"e{typ}"
self._textured_surf(face, self.back_material,
current, color, scale, angle)
except Exception as e:
logger.error(f"添加型材属性失败: {e}")
def _add_part_stretch(self, part, data):
"""创建拉伸部件"""
try:
if not BLENDER_AVAILABLE:
return None
compensates = data.get("compensates", [])
trim_surfs = data.get("trim_surfs", [])
baselines = self._create_paths(part, data.get("baselines", []))
# 尝试加载预制件
inst = None
if ("sid" in data and not compensates and not trim_surfs and len(baselines) == 1):
file_path = f"{SUWood.suwood_path('V_StretchPart')}/{data['sid']}.skp"
# 在实际应用中需要实现文件加载逻辑
# 这里创建占位符
inst = self._load_stretch_prefab(
part, file_path, data, baselines[0])
if inst:
# 创建虚拟几何体
leaf = bpy.data.objects.new("Virtual_Stretch", None)
leaf.parent = part
bpy.context.scene.collection.objects.link(leaf)
surf = data.get("sect", {})
surf["segs"] = data.get("bounds", [])
self._follow_me(leaf, surf, baselines, None)
leaf["sw_virtual"] = True
leaf.hide_viewport = True
else:
# 创建实际几何体
thick = data.get("thick", 18) * 0.001 # mm to meters
leaf = bpy.data.objects.new("Stretch_Part", None)
leaf.parent = part
bpy.context.scene.collection.objects.link(leaf)
zaxis = Vector3d.parse(data.get("zaxis", "(0,0,1)"))
color = data.get("ckey")
sect = data.get("sect", {})
self._follow_me(leaf, sect, baselines, color)
# 处理补偿面
for compensate in compensates:
self._apply_compensate(leaf, compensate, zaxis, thick)
# 处理修剪面
for trim_surf in trim_surfs:
self._apply_trim_surf(leaf, trim_surf, zaxis, thick)
leaf["sw_ckey"] = color
return leaf
except Exception as e:
logger.error(f"创建拉伸部件失败: {e}")
return None
def _add_part_arc(self, part, data, antiz, profiles):
"""创建弧形部件"""
try:
if not BLENDER_AVAILABLE:
return None
leaf = bpy.data.objects.new("Arc_Part", None)
leaf.parent = part
bpy.context.scene.collection.objects.link(leaf)
obv = data.get("obv", {})
color = data.get("ckey")
scale = data.get("scale")
angle = data.get("angle")
color2 = data.get("ckey2")
scale2 = data.get("scale2")
angle2 = data.get("angle2")
# 设置属性
leaf["sw_ckey"] = color
if scale:
leaf["sw_scale"] = scale
if angle:
leaf["sw_angle"] = angle
# 创建弧形路径
center_o = Point3d.parse(data.get("co", "(0,0,0)"))
center_r = Point3d.parse(data.get("cr", "(0,0,0)"))
path = self._create_arc_path(leaf, center_o, center_r)
# 创建弧形几何体
series = []
normal = self._follow_me(
leaf, obv, path, color, scale, angle, False, series, True)
# 处理面和边
if len(series) == 4:
self._process_arc_faces(
leaf, series, normal, center_o, center_r, color2, scale2, angle2, profiles)
return leaf
except Exception as e:
logger.error(f"创建弧形部件失败: {e}")
return None
def _process_arc_faces(self, leaf, series, normal, center_o, center_r, color2, scale2, angle2, profiles):
"""处理弧形面和边"""
try:
count = 0
edge1 = False
edge3 = False
face2 = color2 is None
for child in leaf.children:
if not hasattr(child, 'data') or not child.data:
continue
# 检查是否是平行于法向量的面
if self._is_parallel_to_normal(child, normal):
if self._is_on_plane(center_o, child):
self._add_part_profile(child, 2, profiles)
count += 1
else:
self._add_part_profile(child, 0, profiles)
count += 1
else:
# 处理边
if not edge1 and self._contains_series_points(child, series[1]):
self._add_part_profile(child, 1, profiles)
count += 1
edge1 = True
elif not edge3 and self._contains_series_points(child, series[3]):
self._add_part_profile(child, 3, profiles)
count += 1
edge3 = True
elif not face2 and self._contains_series_points(child, series[2]):
self._textured_surf(
child, self.back_material, color2, color2, scale2, angle2)
count += 1
face2 = True
# 检查是否完成
expected_count = 5 if color2 else 4
if count >= expected_count:
break
except Exception as e:
logger.error(f"处理弧形面失败: {e}")
# ==================== 硬件管理方法 ====================
def c08(self, data: Dict[str, Any]):
"""add_hardware - 添加硬件 - 线程安全版本"""
try:
if not BLENDER_AVAILABLE:
return
uid = data.get("uid")
hardwares = self.get_hardwares(data)
def create_hardware():
try:
items = data.get("items", [])
created_count = 0
for item in items:
root = item.get("root")
file_path = item.get("file")
ps = Point3d.parse(item.get("ps", "(0,0,0)"))
pe = Point3d.parse(item.get("pe", "(0,0,0)"))
if file_path:
hardware = self._load_hardware_file(
file_path, item, ps, pe)
else:
hardware = self._create_simple_hardware(
ps, pe, item)
if hardware:
hardware["sw_uid"] = uid
hardware["sw_root"] = root
hardware["sw_typ"] = "hw"
# 应用单元变换
if uid in self.unit_trans:
self._apply_transformation(
hardware, self.unit_trans[uid])
hardwares[root] = hardware
memory_manager.register_object(hardware)
created_count += 1
return created_count
except Exception as e:
logger.error(f"创建硬件失败: {e}")
return 0
# 在主线程中执行硬件创建
count = create_hardware()
if count > 0:
logger.info(f"✅ 成功创建硬件: uid={uid}, count={count}")
else:
logger.error(f"❌ 硬件创建失败: uid={uid}")
except Exception as e:
logger.error(f"❌ 添加硬件失败: {e}")
def _load_hardware_file(self, file_path, item, ps, pe):
"""加载硬件文件"""
try:
# 在实际应用中需要实现文件加载逻辑
# 这里创建占位符
elem = bpy.data.objects.new(
f"Hardware_{item.get('uid', 'unknown')}", None)
bpy.context.scene.collection.objects.link(elem)
# 设置缩放
if ps and pe:
distance = math.sqrt((pe.x - ps.x)**2 +
(pe.y - ps.y)**2 + (pe.z - ps.z)**2)
elem.scale = (distance, 1.0, 1.0)
# 应用变换
if "trans" in item:
trans = Transformation.parse(item["trans"])
self._apply_transformation(elem, trans)
return elem
except Exception as e:
logger.error(f"加载硬件文件失败: {e}")
return None
def _create_simple_hardware(self, ps, pe, item):
"""创建简单硬件几何体"""
try:
elem = bpy.data.objects.new("Simple_Hardware", None)
bpy.context.scene.collection.objects.link(elem)
# 创建路径
path = self._create_line_path(ps, pe)
# 创建截面
sect = item.get("sect", {})
color = item.get("ckey")
self._follow_me(elem, sect, path, color)
elem["sw_ckey"] = color
return elem
except Exception as e:
logger.error(f"创建简单硬件失败: {e}")
return None
# ==================== 加工管理方法 ====================
def c05(self, data: Dict[str, Any]):
"""c05 - 添加加工 - 批量优化版本"""
try:
if not BLENDER_AVAILABLE:
logger.warning("Blender 不可用,跳过加工创建")
return
uid = data.get("uid")
items = data.get("items", [])
logger.info(f"🔧 开始批量创建加工: uid={uid}, 项目数={len(items)}")
def create_machining_batch():
try:
# 获取部件和硬件集合
parts = self.get_parts(data)
hardwares = self.get_hardwares(data)
# 初始化加工集合
if uid not in self.machinings:
self.machinings[uid] = []
machinings = self.machinings[uid]
# 分类处理:可视化加工 vs 布尔运算
visual_works = []
boolean_works = []
for i, work in enumerate(items):
if work.get("cancel", 0) == 1:
continue
cp = work.get("cp")
if not cp:
continue
# 获取组件
component = None
if cp in parts:
component = parts[cp]
elif cp in hardwares:
component = hardwares[cp]
if not component or not self._is_object_valid(component):
continue
work['component'] = component
work['index'] = i
if work.get("trim3d", 0) == 1:
boolean_works.append(work)
else:
visual_works.append(work)
created_count = 0
# 1. 批量处理可视化加工
if visual_works:
created_count += self._create_visual_machining_batch(visual_works, machinings)
# 2. 批量处理布尔运算
if boolean_works:
created_count += self._create_boolean_machining_batch(boolean_works)
logger.info(f"📊 批量加工创建完成: {created_count}/{len(items)} 成功")
return created_count
except Exception as e:
logger.error(f"❌ 批量创建加工失败: {e}")
import traceback
logger.error(traceback.format_exc())
return 0
# 直接执行创建(已经在主线程中)
count = create_machining_batch()
logger.info(f"🎉 c05命令完成创建了 {count} 个加工对象")
return count
except Exception as e:
logger.error(f"❌ c05命令失败: {e}")
import traceback
logger.error(traceback.format_exc())
return 0
def _create_visual_machining_batch(self, visual_works, machinings):
"""批量创建可视化加工对象"""
try:
import bmesh
created_count = 0
# 按组件分组,同一组件的加工可以批量创建
component_groups = {}
for work in visual_works:
component = work['component']
if component not in component_groups:
component_groups[component] = []
component_groups[component].append(work)
for component, works in component_groups.items():
logger.info(f"🔨 为组件 {component.name} 批量创建 {len(works)} 个加工对象")
# 创建主加工组
main_machining = bpy.data.objects.new(f"Machining_{component.name}", None)
bpy.context.scene.collection.objects.link(main_machining)
main_machining.parent = component
main_machining["sw_typ"] = "work"
machinings.append(main_machining)
# 使用bmesh批量创建所有几何体
bm = bmesh.new()
for work in works:
try:
# 解析坐标
p1 = self._parse_point3d(work.get("p1", "(0,0,0)"))
p2 = self._parse_point3d(work.get("p2", "(0,0,0)"))
# 根据类型创建几何体
if "tri" in work:
self._add_triangle_to_bmesh(bm, work, p1, p2)
elif "surf" in work:
self._add_surface_to_bmesh(bm, work, p1, p2)
else:
self._add_circle_to_bmesh(bm, work, p1, p2)
created_count += 1
except Exception as e:
logger.error(f"创建单个加工几何体失败: {e}")
# 一次性创建网格
if bm.verts:
mesh = bpy.data.meshes.new(f"MachiningMesh_{component.name}")
bm.to_mesh(mesh)
mesh.update()
# 创建对象
mesh_obj = bpy.data.objects.new(f"MachiningGeometry_{component.name}", mesh)
bpy.context.scene.collection.objects.link(mesh_obj)
mesh_obj.parent = main_machining
# 设置材质
material = self.get_texture("mat_machine")
if material and mesh_obj.data:
mesh_obj.data.materials.clear()
mesh_obj.data.materials.append(material)
# 注册到内存管理器
memory_manager.register_object(mesh_obj)
memory_manager.register_mesh(mesh)
bm.free()
memory_manager.register_object(main_machining)
return created_count
except Exception as e:
logger.error(f"批量创建可视化加工失败: {e}")
return 0
def _create_boolean_machining_batch(self, boolean_works):
"""批量创建布尔运算加工 - 关键优化"""
try:
import bmesh
logger.info(f"🔨 开始批量布尔运算: {len(boolean_works)} 个项目")
# 按组件分组
component_groups = {}
for work in boolean_works:
component = work['component']
if component not in component_groups:
component_groups[component] = []
component_groups[component].append(work)
success_count = 0
for component, works in component_groups.items():
logger.info(f"🔨 为组件 {component.name} 处理 {len(works)} 个布尔运算")
# 🎯 关键优化:按类型分组裁剪体
circle_trimmers = []
triangle_trimmers = []
surface_trimmers = []
# 使用bmesh批量创建裁剪体
for work in works:
try:
p1 = self._parse_point3d(work.get("p1", "(0,0,0)"))
p2 = self._parse_point3d(work.get("p2", "(0,0,0)"))
if "tri" in work:
trimmer_data = self._create_triangle_trimmer_data(work, p1, p2)
triangle_trimmers.append(trimmer_data)
elif "surf" in work:
trimmer_data = self._create_surface_trimmer_data(work, p1, p2)
surface_trimmers.append(trimmer_data)
else:
trimmer_data = self._create_circle_trimmer_data(work, p1, p2)
circle_trimmers.append(trimmer_data)
except Exception as e:
logger.error(f"创建裁剪体数据失败: {e}")
# 🚀 超级优化:合并同类型裁剪体
unified_trimmers = []
if circle_trimmers:
unified_trimmer = self._create_unified_circle_trimmer(circle_trimmers, component.name)
if unified_trimmer:
unified_trimmers.append(unified_trimmer)
if triangle_trimmers:
unified_trimmer = self._create_unified_triangle_trimmer(triangle_trimmers, component.name)
if unified_trimmer:
unified_trimmers.append(unified_trimmer)
if surface_trimmers:
unified_trimmer = self._create_unified_surface_trimmer(surface_trimmers, component.name)
if unified_trimmer:
unified_trimmers.append(unified_trimmer)
# 获取所有需要被切割的板材
target_boards = []
for child in component.children:
if child.get("sw_typ") == "board" or "Board" in child.name:
target_boards.append(child)
if target_boards and unified_trimmers:
# 批量应用布尔运算
for unified_trimmer in unified_trimmers:
if self._apply_batch_boolean(target_boards, unified_trimmer):
success_count += 1
logger.info(f"✅ 统一裁剪体布尔运算成功: {unified_trimmer.name}")
else:
logger.warning(f"⚠️ 统一裁剪体布尔运算失败: {unified_trimmer.name}")
# 清理裁剪体
for trimmer in unified_trimmers:
if trimmer and trimmer.name in bpy.data.objects:
bpy.data.objects.remove(trimmer, do_unlink=True)
logger.info(f"📊 批量布尔运算完成: {success_count} 个成功")
return success_count
except Exception as e:
logger.error(f"批量布尔运算失败: {e}")
return 0
def _add_circle_to_bmesh(self, bm, work, p1, p2):
"""向bmesh添加圆形几何体"""
try:
dia = work.get("dia", 5.0)
radius = dia * 0.001 / 2.0
# 计算方向和位置
direction = (p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
length = (direction[0]**2 + direction[1]**2 + direction[2]**2)**0.5
center = ((p1[0] + p2[0])/2, (p1[1] + p2[1])/2, (p1[2] + p2[2])/2)
# 创建圆柱体
bmesh.ops.create_cone(
bm,
cap_ends=True, # 生成端盖
cap_tris=False, # 端盖用 n 边而非三角
segments=12,
diameter1=radius * 2,
diameter2=radius * 2, # 与 diameter1 相同 → 圆柱
depth=max(length, 0.01)
)
# 移动到正确位置
bmesh.ops.translate(
bm,
vec=center,
verts=bm.verts[-24:] # 圆柱体的顶点
)
except Exception as e:
logger.error(f"添加圆形到bmesh失败: {e}")
def _add_triangle_to_bmesh(self, bm, work, p1, p2):
"""向bmesh添加三角形几何体"""
try:
tri = self._parse_point3d(work.get("tri", "(0,0,0)"))
# 创建三角形顶点
v1 = bm.verts.new(tri)
v2 = bm.verts.new((tri[0] + p2[0] - p1[0], tri[1] + p2[1] - p1[1], tri[2] + p2[2] - p1[2]))
v3 = bm.verts.new((p1[0] + p1[0] - tri[0], p1[1] + p1[1] - tri[1], p1[2] + p1[2] - tri[2]))
# 创建面
bm.faces.new([v1, v2, v3])
except Exception as e:
logger.error(f"添加三角形到bmesh失败: {e}")
def _add_surface_to_bmesh(self, bm, work, p1, p2):
"""向bmesh添加表面几何体"""
try:
surf = work.get("surf")
if not surf:
return
# 解析表面顶点
vertices = self._parse_surface_vertices(surf)
if len(vertices) < 3:
return
# 添加顶点到bmesh
bm_verts = []
for vertex in vertices:
bm_verts.append(bm.verts.new(vertex))
# 创建面
if len(bm_verts) >= 3:
bm.faces.new(bm_verts)
except Exception as e:
logger.error(f"添加表面到bmesh失败: {e}")
def _create_unified_circle_trimmer(self, circle_data_list, component_name):
"""创建统一的圆形裁剪体 - 关键优化"""
try:
import bmesh
if not circle_data_list:
return None
logger.info(f"🔄 合并 {len(circle_data_list)} 个圆形裁剪体")
# 创建bmesh
bm = bmesh.new()
# 批量添加所有圆柱体
for circle_data in circle_data_list:
try:
# 创建圆柱体
bmesh.ops.create_cone(
bm,
cap_ends=True, # 生成端盖
cap_tris=False, # 端盖用 n 边而非三角
segments=12,
diameter1=circle_data['radius'] * 2,
diameter2=circle_data['radius'] * 2, # 与 diameter1 相同 → 圆柱
depth=circle_data['depth']
)
# 移动到正确位置
recent_verts = bm.verts[-24:] # 最近创建的圆柱体顶点
bmesh.ops.translate(
bm,
vec=circle_data['location'],
verts=recent_verts
)
# 旋转到正确方向
if circle_data.get('rotation'):
bmesh.ops.rotate(
bm,
cent=circle_data['location'],
matrix=circle_data['rotation'],
verts=recent_verts
)
except Exception as e:
logger.error(f"添加单个圆柱体失败: {e}")
if not bm.verts:
bm.free()
return None
# 🚀 关键:合并重叠的几何体
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
# 创建最终的统一裁剪体
mesh = bpy.data.meshes.new(f"UnifiedCircleTrimmer_{component_name}")
bm.to_mesh(mesh)
mesh.update()
bm.free()
trimmer_obj = bpy.data.objects.new(f"UnifiedCircleTrimmer_{component_name}", mesh)
bpy.context.scene.collection.objects.link(trimmer_obj)
trimmer_obj["sw_typ"] = "trimmer"
trimmer_obj["sw_temporary"] = True
memory_manager.register_object(trimmer_obj)
memory_manager.register_mesh(mesh)
return trimmer_obj
except Exception as e:
logger.error(f"创建统一圆形裁剪体失败: {e}")
return None
def _create_circle_trimmer_data(self, work, p1, p2):
"""创建圆形裁剪体数据"""
try:
dia = work.get("dia", 5.0)
radius = dia * 0.001 / 2.0
# 计算长度和位置
direction = (p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
length = (direction[0]**2 + direction[1]**2 + direction[2]**2)**0.5
location = ((p1[0] + p2[0])/2, (p1[1] + p2[1])/2, (p1[2] + p2[2])/2)
# 计算旋转矩阵(简化版本)
rotation = None
if length > 0.001:
# 这里可以添加更精确的旋转计算
pass
return {
'radius': radius,
'depth': max(length, 0.01),
'location': location,
'rotation': rotation
}
except Exception as e:
logger.error(f"创建圆形裁剪体数据失败: {e}")
return None
def _create_unified_triangle_trimmer(self, triangle_data_list, component_name):
"""创建统一的三角形裁剪体"""
try:
import bmesh
if not triangle_data_list:
return None
logger.info(f"🔄 合并 {len(triangle_data_list)} 个三角形裁剪体")
bm = bmesh.new()
# 批量添加所有三角形
for tri_data in triangle_data_list:
try:
vertices = tri_data['vertices']
if len(vertices) >= 3:
bm_verts = []
for vertex in vertices:
bm_verts.append(bm.verts.new(vertex))
bm.faces.new(bm_verts)
except Exception as e:
logger.error(f"添加单个三角形失败: {e}")
if not bm.verts:
bm.free()
return None
# 挤出三角形形成体积
bmesh.ops.solidify(bm, geom=bm.faces[:], thickness=0.01)
# 创建最终对象
mesh = bpy.data.meshes.new(f"UnifiedTriangleTrimmer_{component_name}")
bm.to_mesh(mesh)
mesh.update()
bm.free()
trimmer_obj = bpy.data.objects.new(f"UnifiedTriangleTrimmer_{component_name}", mesh)
bpy.context.scene.collection.objects.link(trimmer_obj)
trimmer_obj["sw_typ"] = "trimmer"
trimmer_obj["sw_temporary"] = True
memory_manager.register_object(trimmer_obj)
memory_manager.register_mesh(mesh)
return trimmer_obj
except Exception as e:
logger.error(f"创建统一三角形裁剪体失败: {e}")
return None
def _create_triangle_trimmer_data(self, work, p1, p2):
"""创建三角形裁剪体数据"""
try:
tri = self._parse_point3d(work.get("tri", "(0,0,0)"))
vertices = [
tri,
(tri[0] + p2[0] - p1[0], tri[1] + p2[1] - p1[1], tri[2] + p2[2] - p1[2]),
(p1[0] + p1[0] - tri[0], p1[1] + p1[1] - tri[1], p1[2] + p1[2] - tri[2])
]
return {
'vertices': vertices
}
except Exception as e:
logger.error(f"创建三角形裁剪体数据失败: {e}")
return None
def _create_unified_surface_trimmer(self, surface_data_list, component_name):
"""创建统一的表面裁剪体"""
try:
import bmesh
if not surface_data_list:
return None
logger.info(f"🔄 合并 {len(surface_data_list)} 个表面裁剪体")
bm = bmesh.new()
# 批量添加所有表面
for surf_data in surface_data_list:
try:
vertices = surf_data['vertices']
if len(vertices) >= 3:
bm_verts = []
for vertex in vertices:
bm_verts.append(bm.verts.new(vertex))
bm.faces.new(bm_verts)
except Exception as e:
logger.error(f"添加单个表面失败: {e}")
if not bm.verts:
bm.free()
return None
# 挤出表面形成体积
bmesh.ops.solidify(bm, geom=bm.faces[:], thickness=0.01)
# 创建最终对象
mesh = bpy.data.meshes.new(f"UnifiedSurfaceTrimmer_{component_name}")
bm.to_mesh(mesh)
mesh.update()
bm.free()
trimmer_obj = bpy.data.objects.new(f"UnifiedSurfaceTrimmer_{component_name}", mesh)
bpy.context.scene.collection.objects.link(trimmer_obj)
trimmer_obj["sw_typ"] = "trimmer"
trimmer_obj["sw_temporary"] = True
memory_manager.register_object(trimmer_obj)
memory_manager.register_mesh(mesh)
return trimmer_obj
except Exception as e:
logger.error(f"创建统一表面裁剪体失败: {e}")
return None
def _create_surface_trimmer_data(self, work, p1, p2):
"""创建表面裁剪体数据"""
try:
surf = work.get("surf")
if not surf:
return None
vertices = self._parse_surface_vertices(surf)
if len(vertices) < 3:
return None
return {
'vertices': vertices
}
except Exception as e:
logger.error(f"创建表面裁剪体数据失败: {e}")
return None
def _apply_batch_boolean(self, target_boards, unified_trimmer):
"""批量应用布尔运算 - 最终优化"""
try:
if not target_boards or not unified_trimmer:
return False
logger.info(f"🔨 对 {len(target_boards)} 个板材应用统一布尔运算")
modifier_name = "SUWood_BatchBoolean"
# 为所有目标板材添加布尔修改器
for board in target_boards:
try:
# 避免重复添加
if modifier_name not in board.modifiers:
mod = board.modifiers.new(name=modifier_name, type='BOOLEAN')
mod.operation = 'DIFFERENCE'
mod.object = unified_trimmer
mod.solver = 'EXACT' # 更稳定
except Exception as e:
logger.error(f"为板材 {board.name} 添加布尔修改器失败: {e}")
# 批量应用修改器
bpy.ops.object.select_all(action='DESELECT')
for board in target_boards:
board.select_set(True)
if target_boards:
bpy.context.view_layer.objects.active = target_boards[0]
# 逐个应用修改器(确保稳定性)
for board in target_boards:
try:
bpy.context.view_layer.objects.active = board
if modifier_name in board.modifiers:
bpy.ops.object.modifier_apply(modifier=modifier_name)
except Exception as e:
logger.error(f"应用板材 {board.name} 布尔修改器失败: {e}")
# 移除失败的修改器
if modifier_name in board.modifiers:
board.modifiers.remove(board.modifiers[modifier_name])
return True
except Exception as e:
logger.error(f"批量布尔运算失败: {e}")
return False
def _create_machining_visual(self, component, work, index):
"""创建加工可视化对象 - 参考Ruby实现"""
try:
cp = work.get("cp")
special = work.get("special", 0)
# 创建加工组
machining_name = f"Machining_{cp}_{index}"
machining = bpy.data.objects.new(machining_name, None)
bpy.context.scene.collection.objects.link(machining)
# 设置父子关系
machining.parent = component
# 设置属性
machining["sw_typ"] = "work"
machining["sw_special"] = special
machining["sw_cp"] = cp
# 解析坐标
p1 = self._parse_point3d(work.get("p1", "(0,0,0)"))
p2 = self._parse_point3d(work.get("p2", "(0,0,0)"))
# 创建路径
path = self._create_machining_path(p1, p2)
# 创建面
face_mesh = None
if "tri" in work:
# 三角形加工
face_mesh = self._create_triangle_machining(machining, work, p1, p2)
elif "surf" in work:
# 表面加工
face_mesh = self._create_surface_machining(machining, work, path)
else:
# 圆形加工(钻孔)
face_mesh = self._create_circle_machining(machining, work, p1, p2, path)
if face_mesh:
# 设置材质
if special == 0 and work.get("cancel", 0) == 0:
material = self.get_texture("mat_machine")
if material and face_mesh.data:
face_mesh.data.materials.clear()
face_mesh.data.materials.append(material)
# 执行Follow Me操作
self._apply_follow_me_to_machining(face_mesh, path)
# 清理路径
if path:
self._cleanup_path(path)
# 注册到内存管理器
memory_manager.register_object(machining)
if face_mesh:
memory_manager.register_object(face_mesh)
return machining
except Exception as e:
logger.error(f"创建加工可视化失败: {e}")
return None
def _create_triangle_machining(self, machining, work, p1, p2):
"""创建三角形加工"""
try:
tri = self._parse_point3d(work.get("tri", "(0,0,0)"))
p3 = self._parse_point3d(work.get("p3", "(0,0,0)"))
# 创建三角形面
vertices = [tri, (tri[0] + p2[0] - p1[0], tri[1] + p2[1] - p1[1], tri[2] + p2[2] - p1[2]),
(p1[0] + p1[0] - tri[0], p1[1] + p1[1] - tri[1], p1[2] + p1[2] - tri[2])]
mesh = bpy.data.meshes.new(f"TriMachining_{machining.name}")
mesh.from_pydata(vertices, [], [(0, 1, 2)])
mesh.update()
face_obj = bpy.data.objects.new(f"TriFace_{machining.name}", mesh)
bpy.context.scene.collection.objects.link(face_obj)
face_obj.parent = machining
return face_obj
except Exception as e:
logger.error(f"创建三角形加工失败: {e}")
return None
def _create_surface_machining(self, machining, work, path):
"""创建表面加工"""
try:
surf = work.get("surf")
if not surf:
return None
# 使用现有的create_face_safe方法
face_obj = self.create_face_safe(machining, surf)
return face_obj
except Exception as e:
logger.error(f"创建表面加工失败: {e}")
return None
def _create_circle_machining(self, machining, work, p1, p2, path):
"""创建圆形加工(钻孔)"""
try:
dia = work.get("dia", 5.0) # 默认5mm直径
radius = dia * 0.001 / 2.0 # 转换为米并计算半径
# 计算方向向量
direction = (p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
length = (direction[0]**2 + direction[1]**2 + direction[2]**2)**0.5
if length < 0.001: # 避免零长度
direction = (0, 0, 1)
length = 1.0
else:
direction = (direction[0]/length, direction[1]/length, direction[2]/length)
# 创建圆形网格
bpy.ops.mesh.primitive_circle_add(
radius=radius,
location=p1,
vertices=16
)
circle_obj = bpy.context.active_object
circle_obj.name = f"CircleMachining_{machining.name}"
circle_obj.parent = machining
# 设置方向(简化版本,避免复杂的旋转计算)
if abs(direction[2]) < 0.99: # 不是垂直方向
# 简单的方向设置避免mathutils导入
circle_obj.rotation_euler = (0, 1.5708, 0) # 90度旋转
return circle_obj
except Exception as e:
logger.error(f"创建圆形加工失败: {e}")
return None
def _create_machining_path(self, p1, p2):
"""创建加工路径"""
try:
# 创建简单的线段路径
mesh = bpy.data.meshes.new("MachiningPath")
mesh.from_pydata([p1, p2], [(0, 1)], [])
mesh.update()
path_obj = bpy.data.objects.new("MachiningPath", mesh)
bpy.context.scene.collection.objects.link(path_obj)
return path_obj
except Exception as e:
logger.error(f"创建加工路径失败: {e}")
return None
def _apply_follow_me_to_machining(self, face_obj, path):
"""对加工对象应用Follow Me操作"""
try:
if not face_obj or not path:
return
# 选择面对象
bpy.context.view_layer.objects.active = face_obj
face_obj.select_set(True)
# 进入编辑模式
bpy.ops.object.mode_set(mode='EDIT')
# 选择所有面
bpy.ops.mesh.select_all(action='SELECT')
# 添加螺旋修改器来模拟Follow Me
bpy.ops.object.mode_set(mode='OBJECT')
# 添加Array修改器沿路径复制
array_mod = face_obj.modifiers.new(name="MachiningArray", type='ARRAY')
array_mod.fit_type = 'FIT_LENGTH'
array_mod.fit_length = self._calculate_path_length(path)
array_mod.count = max(2, int(array_mod.fit_length / 0.01)) # 每1cm一个
# 应用修改器
bpy.context.view_layer.objects.active = face_obj
bpy.ops.object.modifier_apply(modifier="MachiningArray")
except Exception as e:
logger.error(f"应用Follow Me失败: {e}")
def _calculate_path_length(self, path):
"""计算路径长度"""
try:
if not path or not path.data:
return 0.01
# 获取顶点
vertices = path.data.vertices
if len(vertices) < 2:
return 0.01
# 计算两点间距离
p1 = vertices[0].co
p2 = vertices[1].co
distance = ((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2 + (p2[2] - p1[2])**2)**0.5
return max(distance, 0.01)
except Exception as e:
logger.error(f"计算路径长度失败: {e}")
return 0.01
def _work_trimmed(self, component, work):
"""执行布尔运算加工 - 最快的布尔实现"""
try:
logger.info(f"🔨 开始布尔运算: component={component.name}")
# 获取组件的所有子对象(板材)
boards = []
for child in component.children:
if child.get("sw_typ") == "board" or "Board" in child.name:
boards.append(child)
if not boards:
logger.warning("没有找到可以进行布尔运算的板材")
return False
# 创建布尔切削器
trimmer = self._create_boolean_trimmer(component, work)
if not trimmer:
logger.error("创建布尔切削器失败")
return False
# 对每个板材执行布尔运算
success_count = 0
for board in boards:
try:
# 执行最快的布尔运算
if self._apply_fast_boolean(board, trimmer, work):
success_count += 1
logger.info(f"✅ 板材 {board.name} 布尔运算成功")
else:
logger.warning(f"⚠️ 板材 {board.name} 布尔运算失败")
except Exception as e:
logger.error(f"板材 {board.name} 布尔运算异常: {e}")
# 清理切削器
self._cleanup_trimmer(trimmer)
logger.info(f"📊 布尔运算统计: {success_count}/{len(boards)} 成功")
return success_count > 0
except Exception as e:
logger.error(f"布尔运算失败: {e}")
return False
def _create_boolean_trimmer(self, component, work):
"""创建布尔切削器"""
try:
# 解析坐标
p1 = self._parse_point3d(work.get("p1", "(0,0,0)"))
p2 = self._parse_point3d(work.get("p2", "(0,0,0)"))
# 创建切削器对象
trimmer_name = f"Trimmer_{component.name}"
if "tri" in work:
# 三角形切削器
trimmer = self._create_triangle_trimmer(trimmer_name, work, p1, p2)
elif "surf" in work:
# 表面切削器
trimmer = self._create_surface_trimmer(trimmer_name, work)
else:
# 圆形切削器
trimmer = self._create_circle_trimmer(trimmer_name, work, p1, p2)
if trimmer:
# 设置属性
trimmer["sw_typ"] = "trimmer"
trimmer["sw_temporary"] = True
# 注册到内存管理器
memory_manager.register_object(trimmer)
return trimmer
except Exception as e:
logger.error(f"创建布尔切削器失败: {e}")
return None
def _create_circle_trimmer(self, name, work, p1, p2):
"""创建圆形切削器"""
try:
dia = work.get("dia", 5.0)
radius = dia * 0.001 / 2.0 # 转换为米
# 计算长度
length = ((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2 + (p2[2] - p1[2])**2)**0.5
length = max(length, 0.01) # 最小1cm
# 创建圆柱体
bpy.ops.mesh.primitive_cylinder_add(
radius=radius,
depth=length,
location=((p1[0] + p2[0])/2, (p1[1] + p2[1])/2, (p1[2] + p2[2])/2)
)
trimmer = bpy.context.active_object
trimmer.name = name
# 设置方向
direction = (p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
if length > 0.001:
direction = (direction[0]/length, direction[1]/length, direction[2]/length)
# 设置旋转使圆柱体沿正确方向(简化版本)
if abs(direction[2]) < 0.99:
# 简单的方向设置避免mathutils导入
trimmer.rotation_euler = (0, 1.5708, 0) # 90度旋转
return trimmer
except Exception as e:
logger.error(f"创建圆形切削器失败: {e}")
return None
def _apply_fast_boolean(self, board, trimmer, work):
"""应用最快的布尔运算"""
try:
# 选择板材
bpy.context.view_layer.objects.active = board
board.select_set(True)
trimmer.select_set(True)
# 添加布尔修改器
bool_mod = board.modifiers.new(name="BooleanTrimmer", type='BOOLEAN')
bool_mod.operation = 'DIFFERENCE' # 差集操作
bool_mod.object = trimmer
bool_mod.solver = 'FAST' # 使用快速求解器
# 应用修改器
bpy.context.view_layer.objects.active = board
bpy.ops.object.modifier_apply(modifier="BooleanTrimmer")
# 标记差异面(如果需要)
if work.get("differ", False):
self._mark_differ_faces(board)
return True
except Exception as e:
logger.error(f"应用布尔运算失败: {e}")
return False
def _parse_point3d(self, point_str):
"""解析3D点坐标"""
try:
if not point_str:
return (0, 0, 0)
# 移除括号并分割
coords = point_str.strip('()').split(',')
if len(coords) >= 3:
x = float(coords[0].strip()) * 0.001 # mm转米
y = float(coords[1].strip()) * 0.001
z = float(coords[2].strip()) * 0.001
return (x, y, z)
else:
return (0, 0, 0)
except Exception as e:
logger.error(f"解析3D点失败: {point_str}, {e}")
return (0, 0, 0)
def _create_surface_trimmer(self, name, work):
"""创建表面切削器"""
try:
surf = work.get("surf")
if not surf:
return None
# 使用现有的create_face_safe方法创建表面
trimmer = bpy.data.objects.new(name, None)
bpy.context.scene.collection.objects.link(trimmer)
face_obj = self.create_face_safe(trimmer, surf)
if face_obj:
face_obj.parent = trimmer
return trimmer
except Exception as e:
logger.error(f"创建表面切削器失败: {e}")
return None
def _create_triangle_trimmer(self, name, work, p1, p2):
"""创建三角形切削器"""
try:
tri = self._parse_point3d(work.get("tri", "(0,0,0)"))
p3 = self._parse_point3d(work.get("p3", "(0,0,0)"))
# 创建三角形网格
vertices = [tri, (tri[0] + p2[0] - p1[0], tri[1] + p2[1] - p1[1], tri[2] + p2[2] - p1[2]),
(p1[0] + p1[0] - tri[0], p1[1] + p1[1] - tri[1], p1[2] + p1[2] - tri[2])]
mesh = bpy.data.meshes.new(f"TriTrimmer_{name}")
mesh.from_pydata(vertices, [], [(0, 1, 2)])
mesh.update()
trimmer = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(trimmer)
return trimmer
except Exception as e:
logger.error(f"创建三角形切削器失败: {e}")
return None
def _set_machining_color(self, machining, item):
"""设置加工对象的颜色编码"""
try:
dia = item.get("dia", 0)
special = item.get("special", 0)
# 根据直径和特殊标记设置颜色
if special == 1:
# 特殊加工 - 红色
machining.color = (1.0, 0.2, 0.2, 1.0)
elif "surf" in item:
# 表面加工 - 蓝色
machining.color = (0.2, 0.5, 1.0, 1.0)
elif dia <= 5:
# 小直径钻孔 - 绿色
machining.color = (0.2, 1.0, 0.2, 1.0)
elif dia <= 10:
# 中等直径钻孔 - 黄色
machining.color = (1.0, 1.0, 0.2, 1.0)
else:
# 大直径钻孔 - 橙色
machining.color = (1.0, 0.6, 0.2, 1.0)
except Exception as e:
logger.error(f"设置加工颜色失败: {e}")
def _create_ultra_simple_machining(self, parent_part, item):
"""创建超级简单的加工对象 - 避免卡住"""
try:
# 解析坐标
p1_str = item.get("p1", "(0,0,0)")
p2_str = item.get("p2", "(0,0,0)")
# 简单解析坐标字符串
def parse_coord(coord_str):
coord_str = coord_str.strip('()')
try:
x, y, z = map(float, coord_str.split(','))
return (x * 0.001, y * 0.001, z * 0.001) # mm转米
except:
return (0, 0, 0)
p1 = parse_coord(p1_str)
p2 = parse_coord(p2_str)
# 计算中心点
center = ((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2, (p1[2] + p2[2]) / 2)
# 获取基本信息
cp = item.get("cp")
dia = item.get("dia", 5)
# 创建简单的加工对象名称
import time
timestamp = int(time.time() * 1000000) % 1000000 # 微秒时间戳
machining_name = f"Machining_{cp}_{timestamp}"
# 创建最简单的Empty对象
machining = bpy.data.objects.new(machining_name, None)
machining.location = center
# 设置简单的显示类型
machining.empty_display_type = 'PLAIN_AXES'
machining.empty_display_size = max(0.005, dia * 0.001) # 最小5mm
# 添加到场景
bpy.context.scene.collection.objects.link(machining)
# 设置父对象
machining.parent = parent_part
# 设置基本属性
machining["sw_typ"] = "machining"
machining["sw_cp"] = cp
machining["sw_diameter"] = dia
machining["sw_p1"] = p1_str
machining["sw_p2"] = p2_str
# 设置简单的颜色
if item.get("cancel", 0) == 1:
machining.color = (1.0, 0.0, 0.0, 1.0) # 红色 - 取消
elif "surf" in item:
machining.color = (0.0, 0.0, 1.0, 1.0) # 蓝色 - 表面加工
else:
machining.color = (0.0, 1.0, 0.0, 1.0) # 绿色 - 钻孔
# 注册到内存管理器
memory_manager.register_object(machining)
logger.debug(f"创建简单加工对象: {machining_name} at {center}")
return machining
except Exception as e:
logger.error(f"创建超级简单加工对象失败: {e}")
return None
# ==================== 几何体创建辅助方法 ====================
def create_face(self, container, surface, color=None, scale=None, angle=None,
series=None, reverse_face=False, back_material=True,
saved_color=None, typ=None):
"""创建面"""
try:
if not BLENDER_AVAILABLE:
return None
segs = surface.get("segs", [])
if not segs:
return None
# 创建边
edges = self._create_edges(container, segs, series)
if not edges:
return None
# 创建面
face = self._create_face_from_edges(container, edges)
if not face:
return None
# 处理法向量
if "vz" in surface:
zaxis = Vector3d.parse(surface["vz"])
if series and "vx" in surface:
xaxis = Vector3d.parse(surface["vx"])
if self._should_reverse_face(face, zaxis, reverse_face):
self._reverse_face(face)
elif reverse_face and self._face_normal_matches(face, zaxis):
self._reverse_face(face)
# 设置属性
if typ:
face["sw_typ"] = typ
# 应用材质
if color:
self._textured_surf(face, back_material,
color, saved_color, scale, angle)
else:
self._textured_surf(face, back_material, "mat_normal")
return face
except Exception as e:
logger.error(f"创建面失败: {e}")
return None
def _create_edges(self, container, segments, series=None):
"""创建边"""
try:
if not BLENDER_AVAILABLE:
return []
edges = []
seg_pts = {}
# 解析所有点
for index, segment in enumerate(segments):
pts = []
for point_str in segment:
point = Point3d.parse(point_str)
if point:
pts.append((point.x, point.y, point.z))
seg_pts[index] = pts
# 创建边
for this_i in range(len(segments)):
pts_i = seg_pts[this_i]
pts_p = seg_pts[(this_i - 1) % len(segments)]
pts_n = seg_pts[(this_i + 1) % len(segments)]
if len(pts_i) > 2:
# 多点段
if len(pts_p) > 2:
prev_p = pts_p[-1]
this_p = pts_i[0]
if prev_p != this_p:
edges.append(self._create_line_edge_simple(
container, prev_p, this_p))
# 添加段内边
for j in range(len(pts_i) - 1):
edges.append(self._create_line_edge_simple(
container, pts_i[j], pts_i[j+1]))
if series:
series.append(pts_i)
else:
# 两点段
point_s = pts_p[-1] if len(pts_p) > 2 else pts_i[0]
point_e = pts_n[0] if len(pts_n) > 2 else pts_i[-1]
edges.append(self._create_line_edge_simple(
container, point_s, point_e))
if series:
series.append([point_s, point_e])
return [e for e in edges if e]
except Exception as e:
logger.error(f"创建边失败: {e}")
return []
def _create_line_edge_simple(self, container, start, end):
"""创建简单线边"""
try:
if not BLENDER_AVAILABLE:
return None
# 创建网格
mesh = bpy.data.meshes.new("Simple_Edge")
vertices = [start, end]
edges = [(0, 1)]
mesh.from_pydata(vertices, edges, [])
mesh.update()
# 创建对象
obj = bpy.data.objects.new("Simple_Edge_Obj", mesh)
obj.parent = container
bpy.context.scene.collection.objects.link(obj)
memory_manager.register_mesh(mesh)
memory_manager.register_object(obj)
return obj
except Exception as e:
logger.error(f"创建简单线边失败: {e}")
return None
def _create_face_from_edges(self, container, edges):
"""从边创建面"""
try:
if not BLENDER_AVAILABLE or not edges:
return None
# 收集所有顶点
vertices = []
vertex_map = {}
for edge in edges:
if hasattr(edge, 'data') and edge.data:
for vert in edge.data.vertices:
co = tuple(vert.co)
if co not in vertex_map:
vertex_map[co] = len(vertices)
vertices.append(co)
if len(vertices) < 3:
return None
# 创建面网格
mesh = bpy.data.meshes.new("Face_Mesh")
faces = [list(range(len(vertices)))]
mesh.from_pydata(vertices, [], faces)
mesh.update()
# 创建对象
obj = bpy.data.objects.new("Face_Obj", mesh)
obj.parent = container
bpy.context.scene.collection.objects.link(obj)
memory_manager.register_mesh(mesh)
memory_manager.register_object(obj)
return obj
except Exception as e:
logger.error(f"从边创建面失败: {e}")
return None
def _create_paths(self, container, segments):
"""创建路径"""
try:
if not BLENDER_AVAILABLE:
return []
paths = []
for seg in segments:
if "c" not in seg:
# 直线段
s = Point3d.parse(seg.get("s", "(0,0,0)"))
e = Point3d.parse(seg.get("e", "(0,0,0)"))
path = self._create_line_path(s, e)
if path:
paths.append(path)
else:
# 弧线段
c = Point3d.parse(seg.get("c", "(0,0,0)"))
x = Vector3d.parse(seg.get("x", "(1,0,0)"))
z = Vector3d.parse(seg.get("z", "(0,0,1)"))
r = seg.get("r", 1.0) * 0.001 # mm to meters
a1 = seg.get("a1", 0.0)
a2 = seg.get("a2", math.pi * 2)
n = seg.get("n", 12)
arc_paths = self._create_arc_paths(c, x, z, r, a1, a2, n)
paths.extend(arc_paths)
return paths
except Exception as e:
logger.error(f"创建路径失败: {e}")
return []
def _create_line_path(self, start, end):
"""创建直线路径"""
try:
if not BLENDER_AVAILABLE:
return None
mesh = bpy.data.meshes.new("Line_Path")
vertices = [(start.x, start.y, start.z), (end.x, end.y, end.z)]
edges = [(0, 1)]
mesh.from_pydata(vertices, edges, [])
mesh.update()
obj = bpy.data.objects.new("Line_Path_Obj", mesh)
bpy.context.scene.collection.objects.link(obj)
memory_manager.register_mesh(mesh)
memory_manager.register_object(obj)
return obj
except Exception as e:
logger.error(f"创建直线路径失败: {e}")
return None
def _create_arc_path(self, container, center_o, center_r):
"""创建弧形路径"""
try:
if not BLENDER_AVAILABLE:
return None
mesh = bpy.data.meshes.new("Arc_Path")
vertices = [(center_o.x, center_o.y, center_o.z),
(center_r.x, center_r.y, center_r.z)]
edges = [(0, 1)]
mesh.from_pydata(vertices, edges, [])
mesh.update()
obj = bpy.data.objects.new("Arc_Path_Obj", mesh)
obj.parent = container
bpy.context.scene.collection.objects.link(obj)
memory_manager.register_mesh(mesh)
memory_manager.register_object(obj)
return obj
except Exception as e:
logger.error(f"创建弧形路径失败: {e}")
return None
def _follow_me(self, container, surface, path, color, scale=None, angle=None,
reverse_face=True, series=None, saved_color=None):
"""跟随路径创建几何体"""
try:
if not BLENDER_AVAILABLE:
return None
# 创建截面
face = self.create_face(container, surface, color, scale, angle,
series, reverse_face, self.back_material, saved_color)
if not face:
return None
# 获取法向量
normal = self._get_face_normal(face)
# 执行跟随操作在Blender中需要使用修改器或其他方法
self._apply_follow_me(face, path)
# 隐藏边
self._hide_edges(container)
# 清理路径
if isinstance(path, list):
for p in path:
self._cleanup_path(p)
else:
self._cleanup_path(path)
return normal
except Exception as e:
logger.error(f"跟随路径失败: {e}")
return None
# ==================== 材质和纹理处理方法 ====================
def _textured_surf(self, face, back_material, color, saved_color=None, scale_a=None, angle_a=None):
"""为表面应用纹理"""
try:
if not BLENDER_AVAILABLE or not face:
return
# 保存属性
if saved_color:
face["sw_ckey"] = saved_color
if scale_a:
face["sw_scale"] = scale_a
if angle_a:
face["sw_angle"] = angle_a
# 获取材质
texture = self.get_texture(color)
if not texture:
return
# 应用材质
if hasattr(face, 'data') and face.data:
if not face.data.materials:
face.data.materials.append(texture)
else:
face.data.materials[0] = texture
# 设置背面材质
if back_material or texture.get("alpha", 1.0) < 1.0:
# 在Blender中设置背面材质
pass
# 应用纹理变换
if face.get("sw_ckey") == color:
scale = face.get("sw_scale")
angle = face.get("sw_angle")
if (scale or angle) and not face.get("sw_rt"):
self._rotate_texture(face, scale, angle)
face["sw_rt"] = True
except Exception as e:
logger.error(f"应用表面纹理失败: {e}")
def _rotate_texture(self, face, scale, angle):
"""旋转纹理"""
try:
if not BLENDER_AVAILABLE or not face:
return
# 在Blender中实现纹理旋转
# 这需要操作UV坐标
if hasattr(face, 'data') and face.data:
# 获取UV层
if face.data.uv_layers:
uv_layer = face.data.uv_layers[0]
# 应用缩放和旋转变换
self._apply_uv_transform(uv_layer, scale, angle)
except Exception as e:
logger.error(f"旋转纹理失败: {e}")
def _apply_uv_transform(self, uv_layer, scale, angle):
"""应用UV变换"""
try:
if not scale and not angle:
return
scale_factor = scale if scale else 1.0
angle_rad = math.radians(angle) if angle else 0.0
# 变换UV坐标
for loop in uv_layer.data:
u, v = loop.uv
# 应用缩放
u *= scale_factor
v *= scale_factor
# 应用旋转
if angle_rad != 0:
cos_a = math.cos(angle_rad)
sin_a = math.sin(angle_rad)
new_u = u * cos_a - v * sin_a
new_v = u * sin_a + v * cos_a
u, v = new_u, new_v
loop.uv = (u, v)
except Exception as e:
logger.error(f"应用UV变换失败: {e}")
# ==================== 选择管理方法 ====================
def sel_clear(self):
"""清除选择 - 优化版本,避免阻塞界面"""
try:
if BLENDER_AVAILABLE:
# 【修复】使用非阻塞的直接属性操作,而不是阻塞性操作符
try:
for obj in bpy.data.objects:
if hasattr(obj, 'select_set'):
obj.select_set(False) # 直接设置选择状态,不刷新视口
except:
# 如果直接操作失败,跳过而不是使用阻塞性操作符
pass
self.__class__._selected_uid = None
self.__class__._selected_obj = None
self.__class__._selected_zone = None
self.__class__._selected_part = None
# 清除选择的面、零件和硬件
for face in self.selected_faces:
if face:
self._textured_face(face, False)
self.selected_faces.clear()
for part in self.selected_parts:
if part:
self.textured_part(part, False)
self.selected_parts.clear()
for hw in self.selected_hws:
if hw:
self._textured_hw(hw, False)
self.selected_hws.clear()
logger.info("选择已清除(非阻塞模式)")
except Exception as e:
logger.error(f"清除选择失败: {e}")
def sel_local(self, obj):
"""选择本地对象"""
try:
if not BLENDER_AVAILABLE or not obj:
return
uid = obj.get("sw_uid")
zid = obj.get("sw_zid")
typ = obj.get("sw_typ")
pid = obj.get("sw_pid", -1)
cp = obj.get("sw_cp", -1)
params = {
"uid": uid,
"zid": zid
}
# 检查是否已选择
if typ == "zid":
if (self.__class__._selected_uid == uid and
self.__class__._selected_obj == zid):
return
elif typ == "cp":
if (self.__class__._selected_uid == uid and
(self.__class__._selected_obj == pid or
self.__class__._selected_obj == cp)):
return
else:
self.sel_clear()
return
# 执行选择
if typ == "cp" and self.part_mode:
params["pid"] = pid
params["cp"] = cp
self._sel_part_local(params)
else:
params["pid"] = -1
params["cp"] = -1
self._sel_zone_local(params)
# 发送选择事件
self._set_cmd("r01", params) # select_client
except Exception as e:
logger.error(f"选择本地对象失败: {e}")
def _sel_zone_local(self, data):
"""选择区域(本地)"""
try:
self.sel_clear()
uid = data.get("uid")
zid = data.get("zid")
zones = self.get_zones({"uid": uid})
parts = self.get_parts({"uid": uid})
hardwares = self.get_hardwares({"uid": uid})
# 获取子区域
children = self._get_child_zones(zones, zid, True)
for child in children:
child_id = child.get("zid")
child_zone = zones.get(child_id)
leaf = child.get("leaf")
if not child_zone:
continue
# 处理零件
for v_root, part in parts.items():
if part and part.get("sw_zid") == child_id:
self.textured_part(part, True)
# 处理硬件
for v_root, hw in hardwares.items():
if hw and hw.get("sw_zid") == child_id:
self._textured_hw(hw, True)
# 处理区域显示
if not leaf or self.hide_none:
child_zone.hide_viewport = True
else:
child_zone.hide_viewport = False
# 处理区域面
for face_child in child_zone.children:
if hasattr(face_child, 'data'):
self._textured_face(face_child, True)
# 设置选择状态
if child_id == zid:
self.__class__._selected_uid = uid
self.__class__._selected_obj = zid
self.__class__._selected_zone = child_zone
except Exception as e:
logger.error(f"选择区域失败: {e}")
def _sel_part_local(self, data):
"""选择零件(本地)"""
try:
self.sel_clear()
parts = self.get_parts(data)
hardwares = self.get_hardwares(data)
uid = data.get("uid")
cp = data.get("cp")
if cp in parts:
part = parts[cp]
if part:
self.textured_part(part, True)
self.__class__._selected_part = part
elif cp in hardwares:
hw = hardwares[cp]
if hw:
self._textured_hw(hw, True)
self.__class__._selected_uid = uid
self.__class__._selected_obj = cp
except Exception as e:
logger.error(f"选择零件失败: {e}")
def _get_child_zones(self, zones, zip_id, myself=False):
"""获取子区域"""
try:
children = []
for zid, entity in zones.items():
if entity and entity.get("sw_zip") == zip_id:
grandchildren = self._get_child_zones(zones, zid, False)
child = {
"zid": zid,
"leaf": len(grandchildren) == 0
}
children.append(child)
children.extend(grandchildren)
if myself:
child = {
"zid": zip_id,
"leaf": len(children) == 0
}
children.append(child)
return children
except Exception as e:
logger.error(f"获取子区域失败: {e}")
return []
def _textured_face(self, face, selected):
"""为面应用纹理"""
try:
if selected:
self.selected_faces.append(face)
color = "mat_select" if selected else "mat_normal"
texture = self.get_texture(color)
if texture and hasattr(face, 'data') and face.data:
if not face.data.materials:
face.data.materials.append(texture)
else:
face.data.materials[0] = texture
# 设置背面材质
if self.back_material or texture.get("alpha", 1.0) < 1.0:
# 在Blender中设置背面材质
pass
except Exception as e:
logger.error(f"为面应用纹理失败: {e}")
def textured_part(self, part, selected):
"""为零件应用纹理"""
try:
if not part:
return
# 检查是否有预制件
for child in part.children:
if (hasattr(child, 'type') and child.type == 'MESH' and
child.get("sw_typ") == "cp"):
break
if selected:
self.selected_parts.append(part)
# 处理子对象
for leaf in part.children:
if not leaf or leaf.get("sw_typ") == "work" or leaf.get("sw_typ") == "pull":
continue
# 处理预制件
if hasattr(leaf, 'type') and leaf.type == 'MESH':
if leaf.get("sw_typ") != "cp": # 非零件对象(硬件等)
continue
# 设置可见性
leaf.hide_viewport = not (
selected or self.mat_type == MAT_TYPE_NATURE)
continue
elif leaf.get("sw_virtual", False): # 虚拟部件
leaf.hide_viewport = not (
selected or self.mat_type == MAT_TYPE_NATURE)
# 确定材质类型
nature = None
if selected:
nature = "mat_select"
elif self.mat_type == MAT_TYPE_NATURE:
mn = leaf.get("sw_mn")
if mn == 1:
nature = "mat_obverse" # 门板
elif mn == 2:
nature = "mat_reverse" # 柜体
elif mn == 3:
nature = "mat_thin" # 背板
# 处理面
for entity in leaf.children:
if hasattr(entity, 'data') and entity.data:
color = nature if nature else self._face_color(
entity, leaf)
self._textured_surf(entity, self.back_material, color)
elif hasattr(entity, 'children'): # 组对象
for entity2 in entity.children:
if hasattr(entity2, 'data') and entity2.data:
color = nature if nature else self._face_color(
entity2, leaf)
self._textured_surf(
entity2, self.back_material, color)
except Exception as e:
logger.error(f"为零件应用纹理失败: {e}")
def _face_color(self, face, leaf):
"""获取面的颜色"""
try:
# 检查差异面
if face.get("sw_differ", False):
return "mat_default"
# 检查正反面类型
if self.mat_type == MAT_TYPE_OBVERSE:
typ = face.get("sw_typ")
if typ in ["o", "e1"]:
return "mat_obverse"
elif typ == "e2":
return "mat_thin"
elif typ in ["r", "e0"]:
return "mat_reverse"
# 获取保存的颜色
color = face.get("sw_ckey")
if not color:
color = leaf.get("sw_ckey")
return color
except Exception as e:
logger.error(f"获取面颜色失败: {e}")
return "mat_default"
def _textured_hw(self, hw, selected):
"""为硬件应用纹理"""
try:
if not hw:
return
# 跳过预制件
if hasattr(hw, 'type') and hw.type == 'MESH':
return
if selected:
self.selected_hws.append(hw)
color = "mat_select" if selected else hw.get(
"sw_ckey", "mat_default")
texture = self.get_texture(color)
# 处理硬件的所有面
for entity in hw.children:
if hasattr(entity, 'data') and entity.data:
if texture:
if not entity.data.materials:
entity.data.materials.append(texture)
else:
entity.data.materials[0] = texture
except Exception as e:
logger.error(f"为硬件应用纹理失败: {e}")
# ==================== 删除管理方法 ====================
def c09(self, data: Dict[str, Any]):
"""del_entity - 删除实体 - 完全对应c03/c04创建逻辑的析构函数"""
try:
if not BLENDER_AVAILABLE:
logger.warning("Blender 不可用,跳过删除操作")
return
def delete_entities():
try:
# 确保在主线程中执行
if threading.current_thread() != threading.main_thread():
logger.warning("删除操作转移到主线程执行")
return
# 清除所有选择
self.sel_clear()
uid = data.get("uid")
typ = data.get("typ") # uid/zid/cp/work/hw/pull/wall
oid = data.get("oid", 0)
logger.info(f"🗑️ 开始删除实体: uid={uid}, typ={typ}, oid={oid}")
# 【构造/析构对称性】根据类型执行对应的删除逻辑
if typ == "uid":
# 删除整个单元 - 对应c03/c04的完整创建
self._del_unit_complete(uid)
elif typ == "zid":
# 删除区域 - 对应c03的zone创建
self._del_zone_complete(uid, oid)
elif typ == "cp":
# 删除部件 - 对应c04的part创建
self._del_part_complete(uid, oid)
elif typ == "wall":
# 删除墙体实体
self._del_wall_entity_safe(data, uid, oid)
elif typ == "hw":
# 删除硬件
self._del_hardware_complete(uid, oid)
else:
# 其他类型的删除
self._del_other_entity_safe(data, uid, typ, oid)
# 清理标签和维度标注
self._clear_labels_safe()
# 强制更新视图
bpy.context.view_layer.update()
logger.info(f"✅ 删除实体完成: uid={uid}, typ={typ}, oid={oid}")
except Exception as e:
logger.error(f"删除实体失败: {e}")
import traceback
logger.error(traceback.format_exc())
# 在主线程中执行删除操作
delete_entities()
except Exception as e:
logger.error(f"❌ 删除实体失败: {e}")
def _del_unit_complete(self, uid: str):
"""完整删除单元 - 对应c03/c04的完整创建逻辑"""
try:
logger.info(f"🗑️ 开始完整删除单元: {uid}")
# 1. 删除所有区域 (对应c03创建的zones)
if uid in self.zones:
zones_to_delete = list(self.zones[uid].keys())
for zid in zones_to_delete:
self._del_zone_complete(uid, zid)
# 清空zones字典
del self.zones[uid]
logger.info(f"✅ 清理了单元 {uid} 的所有区域数据")
# 2. 删除所有部件 (对应c04创建的parts)
if uid in self.parts:
parts_to_delete = list(self.parts[uid].keys())
for cp in parts_to_delete:
self._del_part_complete(uid, cp)
# 清空parts字典
del self.parts[uid]
logger.info(f"✅ 清理了单元 {uid} 的所有部件数据")
# 3. 删除所有硬件 (对应c08创建的hardwares)
if uid in self.hardwares:
hardwares_to_delete = list(self.hardwares[uid].keys())
for hw_id in hardwares_to_delete:
self._del_hardware_complete(uid, hw_id)
# 清空hardwares字典
del self.hardwares[uid]
logger.info(f"✅ 清理了单元 {uid} 的所有硬件数据")
# 4. 删除所有加工 (对应c05创建的machinings)
if uid in self.machinings:
del self.machinings[uid]
logger.info(f"✅ 清理了单元 {uid} 的所有加工数据")
# 5. 删除所有尺寸标注 (对应c07创建的dimensions)
if uid in self.dimensions:
del self.dimensions[uid]
logger.info(f"✅ 清理了单元 {uid} 的所有尺寸标注数据")
# 6. 清理单元级别的数据
self._cleanup_unit_data(uid)
# 7. 【新增】清理c15缓存
self._clear_c15_cache(uid)
logger.info(f"🎉 单元 {uid} 完整删除完成")
except Exception as e:
logger.error(f"完整删除单元失败 {uid}: {e}")
def _del_zone_complete(self, uid: str, zid: int):
"""完整删除区域 - 对应c03的zone创建逻辑"""
try:
logger.info(f"🗑️ 开始删除区域: uid={uid}, zid={zid}")
# 1. 找到Zone对象
zone_name = f"Zone_{zid}"
zone_obj = bpy.data.objects.get(zone_name)
if zone_obj:
# 2. 递归删除所有子对象 (对应create_face_safe创建的子面)
children_to_delete = list(zone_obj.children)
for child in children_to_delete:
logger.info(f"删除Zone子对象: {child.name}")
self._delete_object_safe(child)
# 3. 删除Zone对象本身
logger.info(f"删除Zone对象: {zone_name}")
self._delete_object_safe(zone_obj)
else:
logger.warning(f"Zone对象不存在: {zone_name}")
# 4. 从数据结构中移除 (对应c03中的存储逻辑)
if uid in self.zones and zid in self.zones[uid]:
del self.zones[uid][zid]
logger.info(f"✅ 从zones数据结构中移除: uid={uid}, zid={zid}")
logger.info(f"✅ 区域删除完成: uid={uid}, zid={zid}")
except Exception as e:
logger.error(f"删除区域失败 uid={uid}, zid={zid}: {e}")
def _del_part_complete(self, uid: str, cp: int):
"""完整删除部件 - 对应c04的part创建逻辑 - 增强全面删除版本"""
try:
logger.info(f"🗑️ 开始删除部件: uid={uid}, cp={cp}")
# 1. 首先检查数据结构中是否存在
parts_exist_in_data = uid in self.parts and cp in self.parts[uid]
if parts_exist_in_data:
stored_part = self.parts[uid][cp]
logger.info(f"📊 数据结构中找到部件: {stored_part.name if stored_part else 'None'}")
else:
logger.info(f"📊 数据结构中未找到部件: uid={uid}, cp={cp}")
# 2. 查找Blender中的Part对象
part_name = f"Part_{cp}"
part_obj = bpy.data.objects.get(part_name)
deleted_objects_count = 0
if part_obj:
logger.info(f"🎯 在Blender中找到部件对象: {part_name}")
# 3. 递归删除所有子对象 (对应各种板材创建方法)
children_to_delete = list(part_obj.children)
logger.info(f"📦 找到 {len(children_to_delete)} 个子对象需要删除")
for child in children_to_delete:
logger.info(f"🗑️ 删除Part子对象: {child.name}")
# 检查是否是板材
if child.get("sw_face_type") == "board":
logger.info(f"📋 删除板材对象: {child.name}")
success = self._delete_object_safe(child)
if success:
deleted_objects_count += 1
logger.info(f"✅ 子对象删除成功: {child.name}")
else:
logger.warning(f"⚠️ 子对象删除失败: {child.name}")
# 4. 删除Part对象本身
logger.info(f"🗑️ 删除Part对象: {part_name}")
success = self._delete_object_safe(part_obj)
if success:
deleted_objects_count += 1
logger.info(f"✅ Part对象删除成功: {part_name}")
else:
logger.warning(f"⚠️ Part对象删除失败: {part_name}")
else:
logger.warning(f"❌ Part对象不存在: {part_name}")
# 5. 【新增】全面搜索并删除所有可能的相关板材对象
board_patterns = [
f"Board_Part_{cp}", # 标准板材
f"Board_Part_{cp}_default", # 默认板材
f"Board_Surface_Part_{cp}", # 表面板材
]
# 搜索带时间戳的板材
all_objects = list(bpy.data.objects)
for obj in all_objects:
# 检查是否是该Part的板材包含时间戳的情况
if obj.name.startswith(f"Board_Part_{cp}_") and obj.name != f"Board_Part_{cp}_default":
logger.info(f"🔍 发现时间戳板材: {obj.name}")
board_patterns.append(obj.name)
orphaned_boards_deleted = 0
for pattern in board_patterns:
board_obj = bpy.data.objects.get(pattern)
if board_obj:
logger.info(f"🗑️ 删除孤立板材: {pattern}")
success = self._delete_object_safe(board_obj)
if success:
orphaned_boards_deleted += 1
logger.info(f"✅ 孤立板材删除成功: {pattern}")
else:
logger.warning(f"⚠️ 孤立板材删除失败: {pattern}")
# 6. 【新增】搜索所有可能的相关对象(基于属性)
attribute_based_objects = []
for obj in bpy.data.objects:
# 检查对象属性
if (obj.get("sw_uid") == uid and obj.get("sw_cp") == cp) or \
(obj.get("sw_face_type") == "board" and f"Part_{cp}" in obj.name):
attribute_based_objects.append(obj)
if attribute_based_objects:
logger.info(f"🔍 通过属性找到 {len(attribute_based_objects)} 个相关对象")
for obj in attribute_based_objects:
if obj.name not in [o.name for o in [part_obj] + (list(part_obj.children) if part_obj else [])]:
logger.info(f"🗑️ 删除属性相关对象: {obj.name}")
success = self._delete_object_safe(obj)
if success:
orphaned_boards_deleted += 1
logger.info(f"✅ 属性相关对象删除成功: {obj.name}")
# 7. 从数据结构中移除 (对应c04中的存储逻辑)
if parts_exist_in_data:
del self.parts[uid][cp]
logger.info(f"✅ 从parts数据结构中移除: uid={uid}, cp={cp}")
# 检查是否清空了整个uid的parts
if not self.parts[uid]:
logger.info(f"📊 uid={uid} 的所有部件已清空")
else:
logger.info(f"📊 数据结构中没有需要清理的部件引用: uid={uid}, cp={cp}")
# 8. 【新增】显示所有剩余的Part对象用于调试
remaining_parts = [obj for obj in bpy.data.objects if obj.name.startswith("Part_")]
if remaining_parts:
logger.info(f"🔍 场景中剩余的Part对象: {[obj.name for obj in remaining_parts]}")
else:
logger.info("🔍 场景中没有剩余的Part对象")
total_deleted = deleted_objects_count + orphaned_boards_deleted
logger.info(f"🎉 部件删除完成: uid={uid}, cp={cp}, 共删除 {total_deleted} 个对象")
except Exception as e:
logger.error(f"❌ 删除部件失败 uid={uid}, cp={cp}: {e}")
import traceback
logger.error(traceback.format_exc())
def _del_hardware_complete(self, uid: str, hw_id: int):
"""完整删除硬件 - 对应c08的hardware创建逻辑"""
try:
logger.info(f"🗑️ 开始删除硬件: uid={uid}, hw_id={hw_id}")
# 从数据结构中查找并删除硬件对象
if uid in self.hardwares and hw_id in self.hardwares[uid]:
hw_obj = self.hardwares[uid][hw_id]
if hw_obj and self._is_object_valid(hw_obj):
logger.info(f"删除硬件对象: {hw_obj.name}")
self._delete_object_safe(hw_obj)
# 从数据结构中移除
del self.hardwares[uid][hw_id]
logger.info(f"✅ 从hardwares数据结构中移除: uid={uid}, hw_id={hw_id}")
else:
logger.warning(f"硬件不存在: uid={uid}, hw_id={hw_id}")
logger.info(f"✅ 硬件删除完成: uid={uid}, hw_id={hw_id}")
except Exception as e:
logger.error(f"删除硬件失败 uid={uid}, hw_id={hw_id}: {e}")
def _del_other_entity_safe(self, data: Dict[str, Any], uid: str, typ: str, oid: int):
"""删除其他类型实体 - 兼容旧逻辑"""
try:
logger.info(f"🗑️ 删除其他实体: uid={uid}, typ={typ}, oid={oid}")
# 获取相应的实体集合
entities = None
if typ == "work":
# 工作实体,可能需要特殊处理
logger.info(f"删除工作实体: uid={uid}, oid={oid}")
# 这里可以添加具体的工作实体删除逻辑
elif typ == "pull":
# 拉手实体,可能需要特殊处理
logger.info(f"删除拉手实体: uid={uid}, oid={oid}")
# 这里可以添加具体的拉手实体删除逻辑
else:
logger.warning(f"未知实体类型: {typ}")
logger.info(f"✅ 其他实体删除完成: uid={uid}, typ={typ}, oid={oid}")
except Exception as e:
logger.error(f"删除其他实体失败 uid={uid}, typ={typ}, oid={oid}: {e}")
def _del_wall_entity_safe(self, data: Dict[str, Any], uid: str, oid: int):
"""安全删除墙体实体"""
try:
logger.info(f"删除墙体实体: uid={uid}, oid={oid}")
# 查找并删除墙体对象
objects_to_delete = []
for obj in list(bpy.data.objects):
try:
if not self._is_object_valid(obj):
continue
# 检查是否是墙体对象
obj_uid = obj.get("sw_uid")
obj_oid = obj.get("sw_oid")
obj_type = obj.get("sw_typ")
if obj_uid == uid and obj_oid == oid and obj_type == "wall":
objects_to_delete.append(obj)
logger.debug(f"标记删除墙体对象: {obj.name}")
except Exception as e:
logger.warning(f"检查墙体对象失败: {e}")
continue
# 删除找到的墙体对象
deleted_count = 0
for obj in objects_to_delete:
try:
if self._delete_object_safe(obj):
deleted_count += 1
except Exception as e:
logger.error(f"删除墙体对象失败 {obj.name if hasattr(obj, 'name') else 'unknown'}: {e}")
logger.info(f"墙体删除完成: {deleted_count}/{len(objects_to_delete)} 个对象")
except Exception as e:
logger.error(f"删除墙体实体失败: {e}")
def _is_object_valid(self, obj) -> bool:
"""检查对象是否仍然有效"""
try:
# 尝试访问对象的基本属性
_ = obj.name
_ = obj.type
# 检查对象是否仍在数据中
return obj.name in bpy.data.objects
except (ReferenceError, AttributeError):
# 对象已被删除或无效
return False
except Exception:
# 其他错误,假设对象无效
return False
def _delete_object_safe(self, obj) -> bool:
"""安全删除对象 - 极简化版本,避免网格删除冲突"""
try:
# 确保在主线程中执行
if threading.current_thread() != threading.main_thread():
logger.warning("对象删除操作必须在主线程中执行")
return False
if not self._is_object_valid(obj):
logger.debug(f"对象已无效,跳过删除")
return True
obj_name = obj.name
obj_type = obj.type
# 递归删除子对象
children = list(obj.children)
for child in children:
if self._is_object_valid(child):
self._delete_object_safe(child)
# 从所有集合中移除对象
try:
for collection in list(obj.users_collection):
if collection and collection.name in bpy.data.collections:
collection.objects.unlink(obj)
except Exception as e:
logger.warning(f"从集合移除对象失败: {e}")
# 只删除对象让Blender自动处理网格清理
try:
bpy.data.objects.remove(obj, do_unlink=True)
logger.debug(f"删除对象成功: {obj_name}")
return True
except (ReferenceError, AttributeError, RuntimeError) as e:
logger.warning(f"删除对象失败 {obj_name}: {e}")
return False
except Exception as e:
logger.error(f"安全删除对象失败: {e}")
return False
def _cleanup_unit_data(self, uid: str):
"""清理单元相关数据"""
try:
# 清理单元变换数据
if hasattr(self, 'unit_trans') and uid in self.unit_trans:
del self.unit_trans[uid]
# 清理选择状态
if self.__class__._selected_uid == uid:
self.__class__._selected_uid = None
self.__class__._selected_obj = None
self.__class__._selected_zone = None
self.__class__._selected_part = None
logger.debug(f"单元数据清理完成: {uid}")
except Exception as e:
logger.warning(f"清理单元数据失败: {e}")
def _clear_labels_safe(self):
"""安全清理标签"""
try:
# 查找并删除标签对象
labels_to_delete = []
for obj in list(bpy.data.objects):
try:
if not self._is_object_valid(obj):
continue
if (obj.name.startswith("Label_") or
obj.name.startswith("Dimension_") or
obj.get("sw_typ") in ["label", "dimension"]):
labels_to_delete.append(obj)
except Exception:
continue
# 删除标签
for label_obj in labels_to_delete:
self._delete_object_safe(label_obj)
if labels_to_delete:
logger.debug(f"清理标签完成: {len(labels_to_delete)}")
except Exception as e:
logger.error(f"清理标签失败: {e}")
def c0a(self, data: Dict[str, Any]):
"""del_machining - 删除加工 - 线程安全版本"""
try:
if not BLENDER_AVAILABLE:
return
def delete_machining():
try:
uid = data.get("uid")
typ = data.get("typ")
oid = data.get("oid")
special = data.get("special", 1)
if uid not in self.machinings:
return True
machinings = self.machinings[uid]
valid_machinings = []
for machining in machinings:
should_delete = False
if machining and hasattr(machining, 'name') and machining.name in bpy.data.objects:
if typ == "uid":
should_delete = True
else:
attr_value = machining.get(f"sw_{typ}")
should_delete = (attr_value == oid)
if should_delete and special == 0:
machining_special = machining.get(
"sw_special", 0)
should_delete = (machining_special == 0)
if should_delete:
try:
bpy.data.objects.remove(
machining, do_unlink=True)
logger.debug(f"已删除加工: {machining.name}")
except Exception as e:
logger.warning(f"删除加工失败: {e}")
else:
valid_machinings.append(machining)
self.machinings[uid] = valid_machinings
return True
except Exception as e:
logger.error(f"删除加工失败: {e}")
return False
# 在主线程中执行删除操作
success = delete_machining()
if success:
logger.info(f"✅ 加工删除完成")
else:
logger.error(f"❌ 加工删除失败")
except Exception as e:
logger.error(f"❌ 删除加工失败: {e}")
def c0c(self, data: Dict[str, Any]):
"""del_dim - 删除尺寸标注 - 线程安全版本"""
try:
if not BLENDER_AVAILABLE:
return
def delete_dimensions():
try:
uid = data.get("uid")
if uid in self.dimensions:
dimensions = self.dimensions[uid]
for dim in dimensions:
try:
if dim and hasattr(dim, 'name') and dim.name in bpy.data.objects:
bpy.data.objects.remove(
dim, do_unlink=True)
logger.debug(f"已删除尺寸标注: {dim.name}")
except Exception as e:
logger.warning(f"删除尺寸标注失败: {e}")
del self.dimensions[uid]
return True
except Exception as e:
logger.error(f"删除尺寸标注失败: {e}")
return False
# 在主线程中执行删除操作
success = delete_dimensions()
if success:
logger.info(f"✅ 尺寸标注删除完成")
else:
logger.error(f"❌ 尺寸标注删除失败")
except Exception as e:
logger.error(f"❌ 删除尺寸标注失败: {e}")
# ==================== 视图管理方法 ====================
def c15(self, data: Dict[str, Any]):
"""sel_unit - 选择单元 - 智能缓存优化版本"""
try:
import time
start_time = time.time()
# 早期退出检查
if not BLENDER_AVAILABLE:
logger.warning("Blender 不可用,跳过选择单元操作")
return
self.sel_clear()
zones = self.get_zones(data)
if not zones:
logger.info("没有区域数据,跳过选择单元操作")
return
uid = data.get("uid", "default")
current_time = time.time()
# 【优化1】智能缓存检查
cache_hit = False
zones_hash = hash(str(sorted(zones.items())))
if (uid in self._c15_cache['zones_hash'] and
self._c15_cache['zones_hash'][uid] == zones_hash and
current_time < self._c15_cache['cache_valid_until']):
# 缓存命中,使用缓存的叶子区域
leaf_zones = self._c15_cache['leaf_zones'].get(uid, set())
cache_hit = True
logger.debug(f"c15缓存命中: uid={uid}, 叶子区域数={len(leaf_zones)}")
else:
# 缓存未命中,重新计算
leaf_zones = self._precompute_leaf_zones(zones)
# 更新缓存
self._c15_cache['leaf_zones'][uid] = leaf_zones
self._c15_cache['zones_hash'][uid] = zones_hash
self._c15_cache['last_update_time'][uid] = current_time
self._c15_cache['cache_valid_until'] = current_time + 5.0 # 5秒缓存有效期
logger.debug(f"c15缓存更新: uid={uid}, 叶子区域数={len(leaf_zones)}")
# 【优化2】Blender对象缓存
if (current_time > self._c15_cache.get('blender_cache_expire', 0)):
self._c15_cache['blender_objects_cache'] = set(bpy.data.objects.keys())
self._c15_cache['blender_cache_expire'] = current_time + 2.0 # 2秒缓存
valid_blender_objects = self._c15_cache['blender_objects_cache']
# 【优化3】批量处理区域可见性
visibility_changes = []
processed_count = 0
skipped_count = 0
for zid, zone in zones.items():
if not zone:
skipped_count += 1
continue
# 检查对象是否在Blender中存在
if zone.name not in valid_blender_objects:
skipped_count += 1
continue
# 使用预计算的叶子区域状态
is_leaf = zid in leaf_zones
new_visibility = is_leaf and self.hide_none
# 只在需要改变时才设置属性
if hasattr(zone, 'hide_viewport') and zone.hide_viewport != new_visibility:
visibility_changes.append((zone, new_visibility))
processed_count += 1
# 【优化4】批量应用可见性变化
failed_count = 0
for zone, visibility in visibility_changes:
try:
zone.hide_viewport = visibility
except Exception as e:
failed_count += 1
logger.warning(f"设置区域 {zone.name} 可见性失败: {e}")
# 性能统计
end_time = time.time()
execution_time = (end_time - start_time) * 1000 # 转换为毫秒
cache_status = "命中" if cache_hit else "未命中"
logger.info(f"c15命令完成: 缓存{cache_status}, 处理{processed_count}个区域, "
f"跳过{skipped_count}个, 可见性变化{len(visibility_changes)}个, "
f"失败{failed_count}个, 耗时{execution_time:.2f}ms")
except Exception as e:
logger.error(f"选择单元失败: {e}")
import traceback
logger.error(traceback.format_exc())
def _precompute_leaf_zones(self, zones: Dict[str, Any]) -> set:
"""预计算所有叶子区域,避免重复计算 - O(n)复杂度"""
try:
# 构建父子关系映射
parent_to_children = {}
for zid, zone in zones.items():
if not zone:
continue
parent_zip = zone.get("sw_zip")
if parent_zip is not None:
if parent_zip not in parent_to_children:
parent_to_children[parent_zip] = []
parent_to_children[parent_zip].append(zid)
# 找出所有叶子区域(没有子区域的区域)
leaf_zones = set()
for zid in zones.keys():
if zid not in parent_to_children:
leaf_zones.add(zid)
logger.debug(f"预计算完成: 发现{len(leaf_zones)}个叶子区域,总区域数{len(zones)}")
return leaf_zones
except Exception as e:
logger.error(f"预计算叶子区域失败: {e}")
# 降级到原始方法
return set()
def _is_leaf_zone(self, zip_id, zones):
"""检查是否是叶子区域 - 保留向后兼容性"""
try:
for zid, zone in zones.items():
if zone and zone.get("sw_zip") == zip_id:
return False
return True
except Exception as e:
logger.error(f"检查叶子区域失败: {e}")
return True
def _clear_c15_cache(self, uid: str = None):
"""清理c15缓存"""
try:
if uid:
# 清理特定uid的缓存
self._c15_cache['leaf_zones'].pop(uid, None)
self._c15_cache['zones_hash'].pop(uid, None)
self._c15_cache['last_update_time'].pop(uid, None)
logger.debug(f"已清理uid={uid}的c15缓存")
else:
# 清理所有缓存
self._c15_cache['leaf_zones'].clear()
self._c15_cache['zones_hash'].clear()
self._c15_cache['last_update_time'].clear()
self._c15_cache['blender_objects_cache'].clear()
self._c15_cache['cache_valid_until'] = 0
logger.debug("已清理所有c15缓存")
except Exception as e:
logger.warning(f"清理c15缓存失败: {e}")
def _get_c15_cache_stats(self) -> Dict[str, Any]:
"""获取c15缓存统计信息"""
try:
import time
current_time = time.time()
stats = {
'cached_uids': len(self._c15_cache['leaf_zones']),
'cache_valid': current_time < self._c15_cache['cache_valid_until'],
'blender_objects_cached': len(self._c15_cache['blender_objects_cache']),
'cache_age_seconds': current_time - min(self._c15_cache['last_update_time'].values()) if self._c15_cache['last_update_time'] else 0
}
return stats
except Exception as e:
logger.warning(f"获取c15缓存统计失败: {e}")
return {}
def c16(self, data: Dict[str, Any]):
"""sel_zone - 选择区域 - 超级简化版本"""
try:
import time
start_time = time.time()
uid = data.get("uid")
zid = data.get("zid")
logger.info(f"🎯 c16选择区域: uid={uid}, zid={zid}")
# 【超级简化策略】只做最基本的状态设置,避免所有复杂操作
try:
# 1. 简单的状态设置
self.__class__._selected_uid = uid
self.__class__._selected_obj = zid
# 2. 【跳过】复杂的递归区域查找
# 原代码: children = self._get_child_zones(zones, zid, True)
logger.debug("跳过复杂的子区域递归查找")
# 3. 【跳过】材质和纹理设置
# 原代码: self.textured_part(part, True)
logger.debug("跳过材质和纹理设置操作")
# 4. 【跳过】可见性批量设置
# 原代码: child_zone.hide_viewport = True/False
logger.debug("跳过可见性批量设置操作")
# 5. 只做最基本的清理(非阻塞版本)
try:
# 简单清除选择状态不触发Blender操作
if hasattr(self, 'selected_faces'):
self.selected_faces.clear()
if hasattr(self, 'selected_parts'):
self.selected_parts.clear()
except Exception as e:
logger.debug(f"清理选择状态时的预期错误: {e}")
end_time = time.time()
execution_time = (end_time - start_time) * 1000
logger.info(f"✅ c16命令完成: 选择区域 uid={uid}, zid={zid}, 耗时{execution_time:.2f}ms")
except Exception as e:
logger.warning(f"c16基本操作失败: {e}")
# 即使基本操作失败,也不抛出异常,确保不阻塞界面
except Exception as e:
logger.error(f"c16选择区域失败: {e}")
# 绝不抛出异常,确保界面不会卡死
def c17(self, data: Dict[str, Any]):
"""sel_elem - 选择元素 - 超级简化版本"""
try:
import time
start_time = time.time()
uid = data.get("uid")
zid = data.get("zid")
pid = data.get("pid")
logger.info(f"🎯 c17选择元素: uid={uid}, zid={zid}, pid={pid}")
# 【超级简化策略】只做最基本的状态设置
try:
# 1. 简单的状态设置
self.__class__._selected_uid = uid
self.__class__._selected_obj = pid if pid else zid
# 2. 【跳过】part_mode检查和复杂分支
# 原代码: if self.part_mode: self._sel_part_parent(data) else: self._sel_zone_local(data)
logger.debug("跳过part_mode复杂分支处理")
# 3. 【跳过】所有Blender对象操作
logger.debug("跳过所有Blender对象材质和可见性操作")
end_time = time.time()
execution_time = (end_time - start_time) * 1000
logger.info(f"✅ c17命令完成: 选择元素 uid={uid}, obj={pid if pid else zid}, 耗时{execution_time:.2f}ms")
except Exception as e:
logger.warning(f"c17基本操作失败: {e}")
# 不抛出异常,确保不阻塞界面
except Exception as e:
logger.error(f"c17选择元素失败: {e}")
# 绝不抛出异常,确保界面不会卡死
def _sel_part_parent(self, data):
"""选择零件父级 - 原始版本已被c17超级简化版本替代"""
logger.warning("⚠️ 调用了原始的_sel_part_parent方法这可能导致界面阻塞")
try:
# 这个方法已经被超级简化版本替代,不应该被调用
# 如果意外调用,只做最基本的状态设置
uid = data.get("uid")
pid = data.get("pid")
self.__class__._selected_uid = uid
self.__class__._selected_obj = pid
logger.info("已使用简化版本完成_sel_part_parent")
except Exception as e:
logger.error(f"原始_sel_part_parent方法失败: {e}")
# ==================== 门和抽屉功能方法 ====================
def c10(self, data: Dict[str, Any]):
"""set_doorinfo - 设置门信息"""
try:
parts = self.get_parts(data)
doors = data.get("drs", [])
for door in doors:
root = door.get("cp", 0)
door_dir = door.get("dov", "")
ps = Point3d.parse(door.get("ps", "(0,0,0)"))
pe = Point3d.parse(door.get("pe", "(0,0,0)"))
offset = Vector3d.parse(door.get("off", "(0,0,0)"))
if root > 0 and root in parts:
part = parts[root]
part["sw_door_dir"] = door_dir
part["sw_door_ps"] = (ps.x, ps.y, ps.z)
part["sw_door_pe"] = (pe.x, pe.y, pe.z)
part["sw_door_offset"] = (offset.x, offset.y, offset.z)
except Exception as e:
logger.error(f"设置门信息失败: {e}")
def c1a(self, data: Dict[str, Any]):
"""open_doors - 开门"""
try:
uid = data.get("uid")
parts = self.get_parts(data)
hardwares = self.get_hardwares(data)
mydoor = data.get("cp", 0)
value = data.get("v", False)
for root, part in parts.items():
if mydoor != 0 and mydoor != root:
continue
door_type = part.get("sw_door", 0)
if door_type <= 0:
continue
is_open = part.get("sw_door_open", False)
if is_open == value:
continue
if door_type not in [10, 15]:
continue
# 获取门的参数
door_ps = part.get("sw_door_ps")
door_pe = part.get("sw_door_pe")
door_off = part.get("sw_door_offset")
if not all([door_ps, door_pe, door_off]):
continue
# 应用单位变换
if uid in self.unit_trans:
trans = self.unit_trans[uid]
door_ps = self._transform_point(door_ps, trans)
door_pe = self._transform_point(door_pe, trans)
door_off = self._transform_vector(door_off, trans)
# 计算变换
if door_type == 10: # 平开门
trans_a = self._calculate_swing_door_transform(
door_ps, door_pe, door_off)
else: # 推拉门
trans_a = self._calculate_slide_door_transform(door_off)
if is_open:
trans_a = self._invert_transform(trans_a)
# 应用变换
self._apply_transformation(part, trans_a)
part["sw_door_open"] = not is_open
# 变换相关硬件
for key, hardware in hardwares.items():
if hardware.get("sw_part") == root:
self._apply_transformation(hardware, trans_a)
except Exception as e:
logger.error(f"开门失败: {e}")
def c1b(self, data: Dict[str, Any]):
"""slide_drawers - 滑动抽屉"""
try:
uid = data.get("uid")
zones = self.get_zones(data)
parts = self.get_parts(data)
hardwares = self.get_hardwares(data)
# 收集抽屉信息
drawers = {}
depths = {}
for root, part in parts.items():
drawer_type = part.get("sw_drawer", 0)
if drawer_type > 0:
if drawer_type == 70: # DR_DP
pid = part.get("sw_pid")
drawer_dir = part.get("sw_drawer_dir")
if pid and drawer_dir:
drawers[pid] = Vector3d(
drawer_dir[0], drawer_dir[1], drawer_dir[2])
if drawer_type in [73, 74]: # DR_LP/DR_RP
pid = part.get("sw_pid")
dr_depth = part.get("sw_dr_depth", 300)
if pid:
depths[pid] = dr_depth
# 计算偏移量
offsets = {}
for drawer, direction in drawers.items():
zone = zones.get(drawer)
if not zone:
continue
dr_depth = depths.get(drawer, 300) * 0.001 # mm to meters
vector = Vector3d(direction.x, direction.y, direction.z)
vector_length = math.sqrt(
vector.x**2 + vector.y**2 + vector.z**2)
if vector_length > 0:
scale = (dr_depth * 0.9) / vector_length
vector = Vector3d(vector.x * scale,
vector.y * scale, vector.z * scale)
# 应用单位变换
if uid in self.unit_trans:
vector = self._transform_vector(
(vector.x, vector.y, vector.z), self.unit_trans[uid])
offsets[drawer] = vector
# 应用抽屉变换
value = data.get("v", False)
for drawer, vector in offsets.items():
zone = zones.get(drawer)
if not zone:
continue
is_open = zone.get("sw_drawer_open", False)
if is_open == value:
continue
# 计算变换
trans_a = self._calculate_translation_transform(vector)
if is_open:
trans_a = self._invert_transform(trans_a)
# 应用到区域
zone["sw_drawer_open"] = not is_open
# 变换零件
for root, part in parts.items():
if part.get("sw_pid") == drawer:
self._apply_transformation(part, trans_a)
# 变换硬件
for root, hardware in hardwares.items():
if hardware.get("sw_pid") == drawer:
self._apply_transformation(hardware, trans_a)
except Exception as e:
logger.error(f"滑动抽屉失败: {e}")
# ==================== 视图控制方法 ====================
def c18(self, data: Dict[str, Any]):
"""hide_door - 隐藏门"""
try:
visible = not data.get("v", False)
if self.door_layer:
# 在Blender中控制集合可见性
self.door_layer.hide_viewport = not visible
if self.door_labels:
self.door_labels.hide_viewport = not visible
except Exception as e:
logger.error(f"隐藏门失败: {e}")
def c28(self, data: Dict[str, Any]):
"""hide_drawer - 隐藏抽屉"""
try:
visible = not data.get("v", False)
if self.drawer_layer:
self.drawer_layer.hide_viewport = not visible
if self.door_labels:
self.door_labels.hide_viewport = not visible
except Exception as e:
logger.error(f"隐藏抽屉失败: {e}")
def c0f(self, data: Dict[str, Any]):
"""view_front - 前视图"""
try:
if BLENDER_AVAILABLE:
# 设置前视图
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
override = {'area': area, 'region': region}
bpy.ops.view3d.view_axis(
override, type='FRONT')
bpy.ops.view3d.view_all(override)
break
except Exception as e:
logger.error(f"前视图失败: {e}")
def c23(self, data: Dict[str, Any]):
"""view_left - 左视图"""
try:
if BLENDER_AVAILABLE:
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
override = {'area': area, 'region': region}
bpy.ops.view3d.view_axis(override, type='LEFT')
bpy.ops.view3d.view_all(override)
break
except Exception as e:
logger.error(f"左视图失败: {e}")
def c24(self, data: Dict[str, Any]):
"""view_right - 右视图"""
try:
if BLENDER_AVAILABLE:
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
override = {'area': area, 'region': region}
bpy.ops.view3d.view_axis(
override, type='RIGHT')
bpy.ops.view3d.view_all(override)
break
except Exception as e:
logger.error(f"右视图失败: {e}")
def c25(self, data: Dict[str, Any]):
"""view_back - 后视图"""
try:
if BLENDER_AVAILABLE:
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
override = {'area': area, 'region': region}
bpy.ops.view3d.view_axis(override, type='BACK')
bpy.ops.view3d.view_all(override)
break
except Exception as e:
logger.error(f"后视图失败: {e}")
# ==================== 其他业务方法 ====================
def c00(self, data: Dict[str, Any]):
"""add_folder - 添加文件夹"""
try:
ref_v = data.get("ref_v", 0)
if ref_v > 0:
# 在实际应用中需要实现文件夹管理逻辑
logger.info(f"添加文件夹: ref_v={ref_v}")
except Exception as e:
logger.error(f"添加文件夹失败: {e}")
def c01(self, data: Dict[str, Any]):
"""edit_unit - 编辑单元"""
try:
unit_id = data.get("unit_id")
if "params" in data:
params = data["params"]
if "trans" in params:
jtran = params.pop("trans")
trans = Transformation.parse(jtran)
self.unit_trans[unit_id] = trans
time.sleep(0.5) # 等待
if unit_id in self.unit_param:
values = self.unit_param[unit_id]
values.update(params)
params = values
self.unit_param[unit_id] = params
except Exception as e:
logger.error(f"编辑单元失败: {e}")
def c07(self, data: Dict[str, Any]):
"""add_dim - 添加尺寸标注 - 线程安全版本"""
try:
if not BLENDER_AVAILABLE:
return
uid = data.get("uid")
def create_dimensions():
try:
dims = data.get("dims", [])
dimensions = []
for dim_data in dims:
p1 = Point3d.parse(dim_data.get("p1", "(0,0,0)"))
p2 = Point3d.parse(dim_data.get("p2", "(0,0,0)"))
direction = Vector3d.parse(
dim_data.get("dir", "(0,0,1)"))
text = dim_data.get("text", "")
dimension = self._create_dimension(
p1, p2, direction, text)
if dimension:
dimensions.append(dimension)
# 存储尺寸标注
if uid not in self.dimensions:
self.dimensions[uid] = []
self.dimensions[uid].extend(dimensions)
for dimension in dimensions:
memory_manager.register_object(dimension)
return len(dimensions)
except Exception as e:
logger.error(f"创建尺寸标注失败: {e}")
return 0
# 在主线程中执行尺寸标注创建
count = create_dimensions()
if count > 0:
logger.info(f"✅ 成功创建尺寸标注: uid={uid}, count={count}")
else:
logger.error(f"❌ 尺寸标注创建失败: uid={uid}")
except Exception as e:
logger.error(f"❌ 添加尺寸标注失败: {e}")
def c0d(self, data: Dict[str, Any]):
"""parts_seqs - 零件序列"""
try:
parts = self.get_parts(data)
seqs = data.get("seqs", [])
for d in seqs:
root = d.get("cp")
seq = d.get("seq")
pos = d.get("pos")
name = d.get("name")
size = d.get("size")
mat = d.get("mat")
e_part = parts.get(root)
if e_part:
e_part["sw_seq"] = seq
e_part["sw_pos"] = pos
if name:
e_part["sw_name"] = name
if size:
e_part["sw_size"] = size
if mat:
e_part["sw_mat"] = mat
except Exception as e:
logger.error(f"零件序列失败: {e}")
def c0e(self, data: Dict[str, Any]):
"""explode_zones - 爆炸视图"""
try:
if not BLENDER_AVAILABLE:
return
# 清理标签
self._clear_labels(self.labels)
self._clear_labels(self.door_labels)
uid = data.get("uid")
zones = self.get_zones(data)
parts = self.get_parts(data)
hardwares = self.get_hardwares(data)
# 处理区域爆炸
jzones = data.get("zones", [])
for zone in jzones:
zoneid = zone.get("zid")
offset = Vector3d.parse(zone.get("vec", "(0,0,0)"))
if uid in self.unit_trans:
offset = self._transform_vector(
(offset.x, offset.y, offset.z), self.unit_trans[uid])
trans_a = self._calculate_translation_transform(offset)
if zoneid in zones:
azone = zones[zoneid]
self._apply_transformation(azone, trans_a)
# 处理零件爆炸
jparts = data.get("parts", [])
for jpart in jparts:
pid = jpart.get("pid")
offset = Vector3d.parse(jpart.get("vec", "(0,0,0)"))
if uid in self.unit_trans:
offset = self._transform_vector(
(offset.x, offset.y, offset.z), self.unit_trans[uid])
trans_a = self._calculate_translation_transform(offset)
# 变换零件
for root, part in parts.items():
if part.get("sw_pid") == pid:
self._apply_transformation(part, trans_a)
# 变换硬件
for root, hardware in hardwares.items():
if hardware.get("sw_pid") == pid:
self._apply_transformation(hardware, trans_a)
# 添加序号标签
if data.get("explode", False):
self._add_part_labels(uid, parts)
except Exception as e:
logger.error(f"爆炸视图失败: {e}")
def _add_part_labels(self, uid, parts):
"""添加零件标签"""
try:
for root, part in parts.items():
center = self._get_object_center(part)
pos = part.get("sw_pos", 1)
# 确定标签方向
if pos == 1:
vector = (0, -1, 0) # F
elif pos == 2:
vector = (0, 1, 0) # K
elif pos == 3:
vector = (-1, 0, 0) # L
elif pos == 4:
vector = (1, 0, 0) # R
elif pos == 5:
vector = (0, 0, -1) # B
else:
vector = (0, 0, 1) # T
# 应用单位变换
if uid in self.unit_trans:
vector = self._transform_vector(
vector, self.unit_trans[uid])
# 创建文本标签
ord_seq = part.get("sw_seq", 0)
text_obj = self._create_text_label(
str(ord_seq), center, vector)
if text_obj:
# 根据图层选择父对象
if self._is_in_door_layer(part):
text_obj.parent = self.door_labels
else:
text_obj.parent = self.labels
except Exception as e:
logger.error(f"添加零件标签失败: {e}")
def c12(self, data: Dict[str, Any]):
"""add_contour - 添加轮廓 - 线程安全版本"""
try:
if not BLENDER_AVAILABLE:
return
def create_contour():
try:
self.added_contour = True
surf = data.get("surf", {})
contour = self._create_contour_from_surf(surf)
if contour:
memory_manager.register_object(contour)
return True
return False
except Exception as e:
logger.error(f"创建轮廓失败: {e}")
return False
# 在主线程中执行轮廓创建
success = create_contour()
if success:
logger.info(f"✅ 成功创建轮廓")
else:
logger.error(f"❌ 轮廓创建失败")
except Exception as e:
logger.error(f"❌ 添加轮廓失败: {e}")
def add_surf(self, data: Dict[str, Any]):
"""add_surf - 添加表面"""
try:
surf = data.get("surf", {})
self.create_face(bpy.context.scene, surf)
except Exception as e:
logger.error(f"添加表面失败: {e}")
def c13(self, data: Dict[str, Any]):
"""save_pixmap - 保存像素图 - 线程安全版本"""
try:
if not BLENDER_AVAILABLE:
return
def save_pixmap():
try:
uid = data.get("uid")
file_path = data.get("file")
# 设置渲染参数
bpy.context.scene.render.filepath = file_path
bpy.context.scene.render.image_settings.file_format = 'PNG'
# 渲染当前视图
bpy.ops.render.render(write_still=True)
return True
except Exception as e:
logger.error(f"保存像素图失败: {e}")
return False
# 在主线程中执行渲染操作
success = save_pixmap()
if success:
logger.info(f"✅ 成功保存像素图")
else:
logger.error(f"❌ 像素图保存失败")
except Exception as e:
logger.error(f"❌ 保存像素图失败: {e}")
def c14(self, data: Dict[str, Any]):
"""pre_save_pixmap - 预保存像素图 - 线程安全版本"""
try:
if not BLENDER_AVAILABLE:
return
def pre_save_pixmap():
try:
self.sel_clear()
# 设置视图
if hasattr(bpy.context, 'space_data') and bpy.context.space_data:
bpy.context.space_data.show_gizmo = False
bpy.context.space_data.show_overlays = False
return True
except Exception as e:
logger.error(f"预保存像素图失败: {e}")
return False
# 在主线程中执行预处理操作
success = pre_save_pixmap()
if success:
logger.info(f"✅ 预保存像素图完成")
else:
logger.error(f"❌ 预保存像素图失败")
except Exception as e:
logger.error(f"❌ 预保存像素图失败: {e}")
def show_message(self, data: Dict[str, Any]):
"""显示消息"""
try:
message = data.get("message", "")
logger.info(f"显示消息: {message}")
# 在Blender中显示消息
if BLENDER_AVAILABLE:
# 可以使用报告系统
pass
except Exception as e:
logger.error(f"显示消息失败: {e}")
# ==================== 辅助方法 ====================
def _set_cmd(self, cmd, params):
"""设置命令(发送到客户端)"""
try:
# 在实际应用中需要实现客户端通信逻辑
logger.info(f"发送命令: {cmd}, 参数: {params}")
except Exception as e:
logger.error(f"设置命令失败: {e}")
def _clear_labels(self, label_obj):
"""清理标签"""
try:
if label_obj and BLENDER_AVAILABLE:
for child in label_obj.children:
bpy.data.objects.remove(child, do_unlink=True)
except Exception as e:
logger.error(f"清理标签失败: {e}")
# ==================== 属性访问器 ====================
@classmethod
def selected_uid(cls):
return cls._selected_uid
@classmethod
def selected_zone(cls):
return cls._selected_zone
@classmethod
def selected_part(cls):
return cls._selected_part
@classmethod
def selected_obj(cls):
return cls._selected_obj
@classmethod
def server_path(cls):
return cls._server_path
@classmethod
def default_zone(cls):
return cls._default_zone
# ==================== 清理和销毁 ====================
def shutdown(self):
"""关闭系统"""
try:
logger.info("开始关闭SUWood系统")
# 执行最终清理
self.force_cleanup()
# 清理所有缓存
self.mesh_cache.clear()
self.material_cache.clear()
self.object_references.clear()
# 清理数据结构
self.parts.clear()
self.zones.clear()
self.hardwares.clear()
self.machinings.clear()
self.dimensions.clear()
self.textures.clear()
self.unit_param.clear()
self.unit_trans.clear()
logger.info("✅ SUWood系统关闭完成")
except Exception as e:
logger.error(f"关闭系统失败: {e}")
def __del__(self):
"""析构函数"""
try:
self.shutdown()
except:
pass
# ==================== 内存管理方法(保持原有的优化) ====================
def force_cleanup(self):
"""强制清理"""
try:
logger.info("开始强制清理")
# 清理孤立数据
cleanup_count = memory_manager.cleanup_orphaned_data()
# 清理缓存
self.mesh_cache.clear()
# 清理过期的对象引用
current_time = time.time()
expired_refs = []
for obj_name, ref_info in self.object_references.items():
if current_time - ref_info.get('creation_time', 0) > 3600: # 1小时
expired_refs.append(obj_name)
for obj_name in expired_refs:
del self.object_references[obj_name]
# 强制垃圾回收
gc.collect()
# 更新依赖图
if BLENDER_AVAILABLE:
self._update_dependency_graph(full_update=True)
logger.info(
f"强制清理完成: 清理了 {cleanup_count} 个数据块,{len(expired_refs)} 个过期引用")
except Exception as e:
logger.error(f"强制清理失败: {e}")
def _update_dependency_graph(self, full_update: bool = False):
"""更新Blender依赖图"""
try:
if not BLENDER_AVAILABLE:
return
if full_update:
logger.info("执行全局依赖图更新")
bpy.context.view_layer.update()
bpy.context.evaluated_depsgraph_get().update()
# 刷新视图
try:
for area in bpy.context.screen.areas:
if area.type in ['VIEW_3D', 'OUTLINER']:
area.tag_redraw()
except:
pass
logger.info("全局依赖图更新完成")
else:
# 快速更新
bpy.context.view_layer.update()
except Exception as e:
logger.error(f"依赖图更新失败: {e}")
def get_creation_stats(self) -> Dict[str, Any]:
"""获取创建统计信息"""
try:
stats = {
"object_references": len(self.object_references),
"mesh_cache_size": len(self.mesh_cache),
"material_cache_size": len(self.material_cache),
"memory_manager_stats": memory_manager.creation_stats.copy(),
"blender_available": BLENDER_AVAILABLE
}
if BLENDER_AVAILABLE:
stats["total_objects"] = len(bpy.data.objects)
stats["total_meshes"] = len(bpy.data.meshes)
stats["total_materials"] = len(bpy.data.materials)
return stats
except Exception as e:
logger.error(f"获取统计信息失败: {e}")
return {"error": str(e)}
def diagnose_system_state(self):
"""诊断系统状态"""
try:
logger.info("=== 系统状态诊断 ===")
# 内存使用情况
stats = self.get_creation_stats()
for key, value in stats.items():
logger.info(f"{key}: {value}")
# 检查潜在问题
issues = []
if BLENDER_AVAILABLE:
# 检查孤立数据
orphaned_meshes = [m for m in bpy.data.meshes if m.users == 0]
if orphaned_meshes:
issues.append(f"发现 {len(orphaned_meshes)} 个孤立网格")
# 检查空网格
empty_meshes = [m for m in bpy.data.meshes if not m.vertices]
if empty_meshes:
issues.append(f"发现 {len(empty_meshes)} 个空网格")
# 检查总顶点数
total_vertices = sum(len(m.vertices) for m in bpy.data.meshes)
if total_vertices > 1000000: # 100万顶点
issues.append(f"顶点数量过多: {total_vertices}")
if issues:
logger.warning("发现问题:")
for issue in issues:
logger.warning(f" - {issue}")
else:
logger.info("✅ 系统状态正常")
return issues
except Exception as e:
logger.error(f"系统诊断失败: {e}")
return [f"诊断失败: {e}"]
def get_memory_report(self) -> Dict[str, Any]:
"""获取内存报告"""
try:
report = {
"timestamp": time.time(),
"creation_stats": self.get_creation_stats(),
"system_diagnosis": self.diagnose_system_state(),
"memory_manager": {
"object_registry_size": len(memory_manager.object_registry),
"mesh_registry_size": len(memory_manager.mesh_registry),
"last_cleanup": memory_manager.last_cleanup_time,
"cleanup_interval": memory_manager.cleanup_interval
}
}
if BLENDER_AVAILABLE:
report["blender_data"] = {
"objects": len(bpy.data.objects),
"meshes": len(bpy.data.meshes),
"materials": len(bpy.data.materials),
"textures": len(bpy.data.textures),
"images": len(bpy.data.images)
}
return report
except Exception as e:
logger.error(f"生成内存报告失败: {e}")
return {"error": str(e)}
# ==================== 几何变换辅助方法 ====================
def _transform_point(self, point, trans):
"""变换点"""
try:
if isinstance(point, (list, tuple)) and len(point) >= 3:
# 简化的变换实现
return (
point[0] + trans.origin.x,
point[1] + trans.origin.y,
point[2] + trans.origin.z
)
return point
except Exception as e:
logger.error(f"变换点失败: {e}")
return point
def _transform_vector(self, vector, trans):
"""变换向量"""
try:
if isinstance(vector, (list, tuple)) and len(vector) >= 3:
# 简化的变换实现
return (
vector[0] * trans.x_axis.x,
vector[1] * trans.y_axis.y,
vector[2] * trans.z_axis.z
)
return vector
except Exception as e:
logger.error(f"变换向量失败: {e}")
return vector
def _calculate_swing_door_transform(self, door_ps, door_pe, door_off):
"""计算平开门变换"""
try:
# 计算旋转轴和角度
axis = (door_pe[0] - door_ps[0], door_pe[1] -
door_ps[1], door_pe[2] - door_ps[2])
angle = math.pi / 2 # 90度
# 在Blender中创建变换矩阵
if BLENDER_AVAILABLE:
import mathutils
rot_matrix = mathutils.Matrix.Rotation(angle, 4, axis)
trans_matrix = mathutils.Matrix.Translation(door_off)
return trans_matrix @ rot_matrix
return None
except Exception as e:
logger.error(f"计算平开门变换失败: {e}")
return None
def _calculate_slide_door_transform(self, door_off):
"""计算推拉门变换"""
try:
if BLENDER_AVAILABLE:
import mathutils
return mathutils.Matrix.Translation(door_off)
return None
except Exception as e:
logger.error(f"计算推拉门变换失败: {e}")
return None
def _calculate_translation_transform(self, vector):
"""计算平移变换"""
try:
if BLENDER_AVAILABLE:
import mathutils
if isinstance(vector, (list, tuple)):
return mathutils.Matrix.Translation(vector)
else:
return mathutils.Matrix.Translation((vector.x, vector.y, vector.z))
return None
except Exception as e:
logger.error(f"计算平移变换失败: {e}")
return None
def _invert_transform(self, transform):
"""反转变换"""
try:
if transform and hasattr(transform, 'inverted'):
return transform.inverted()
return transform
except Exception as e:
logger.error(f"反转变换失败: {e}")
return transform
def _normalize_vector(self, x, y, z):
"""归一化向量"""
try:
length = math.sqrt(x*x + y*y + z*z)
if length > 0:
return (x/length, y/length, z/length)
return (0, 0, 1)
except Exception as e:
logger.error(f"归一化向量失败: {e}")
return (0, 0, 1)
def _get_object_center(self, obj):
"""获取对象中心"""
try:
if BLENDER_AVAILABLE and obj and hasattr(obj, 'location'):
return obj.location
return (0, 0, 0)
except Exception as e:
logger.error(f"获取对象中心失败: {e}")
return (0, 0, 0)
def _is_in_door_layer(self, part):
"""检查是否在门图层"""
try:
if not part or not self.door_layer:
return False
return part in self.door_layer.objects
except Exception as e:
logger.error(f"检查门图层失败: {e}")
return False
def _create_text_label(self, text, location, direction):
"""创建文本标签"""
try:
if not BLENDER_AVAILABLE:
return None
# 创建文本对象
font_curve = bpy.data.curves.new(type="FONT", name="TextLabel")
font_curve.body = text
font_obj = bpy.data.objects.new("TextLabel", font_curve)
# 设置位置和方向
font_obj.location = location
if isinstance(direction, (list, tuple)) and len(direction) >= 3:
# 简化的方向设置
font_obj.location = (
location[0] + direction[0] * 0.1,
location[1] + direction[1] * 0.1,
location[2] + direction[2] * 0.1
)
bpy.context.scene.collection.objects.link(font_obj)
memory_manager.register_object(font_obj)
return font_obj
except Exception as e:
logger.error(f"创建文本标签失败: {e}")
return None
def _create_contour_from_surf(self, surf):
"""从表面创建轮廓"""
try:
if not BLENDER_AVAILABLE:
return
xaxis = Vector3d.parse(surf.get("vx", "(1,0,0)"))
zaxis = Vector3d.parse(surf.get("vz", "(0,0,1)"))
segs = surf.get("segs", [])
edges = []
for seg in segs:
if "c" in seg:
# 弧形段
c = Point3d.parse(seg["c"])
r = seg.get("r", 1.0) * 0.001
a1 = seg.get("a1", 0.0)
a2 = seg.get("a2", math.pi * 2)
n = seg.get("n", 12)
# 创建弧形边
arc_edges = self._create_arc_edges(
c, xaxis, zaxis, r, a1, a2, n)
edges.extend(arc_edges)
else:
# 直线段
s = Point3d.parse(seg.get("s", "(0,0,0)"))
e = Point3d.parse(seg.get("e", "(0,0,0)"))
edge = self._create_line_edge_simple(bpy.context.scene,
(s.x, s.y, s.z),
(e.x, e.y, e.z))
if edge:
edges.append(edge)
# 尝试创建面
try:
if edges:
self._create_face_from_edges(bpy.context.scene, edges)
except Exception as e:
logger.warning(f"创建轮廓面失败: {e}")
except Exception as e:
logger.error(f"创建轮廓失败: {e}")
def _create_arc_edges(self, center, xaxis, zaxis, radius, start_angle, end_angle, segments):
"""创建弧形边"""
try:
if not BLENDER_AVAILABLE:
return []
edges = []
angle_step = (end_angle - start_angle) / segments
for i in range(segments):
angle1 = start_angle + i * angle_step
angle2 = start_angle + (i + 1) * angle_step
# 计算点
x1 = center.x + radius * math.cos(angle1)
y1 = center.y + radius * math.sin(angle1)
z1 = center.z
x2 = center.x + radius * math.cos(angle2)
y2 = center.y + radius * math.sin(angle2)
z2 = center.z
edge = self._create_line_edge_simple(bpy.context.scene,
(x1, y1, z1),
(x2, y2, z2))
if edge:
edges.append(edge)
return edges
except Exception as e:
logger.error(f"创建弧形边失败: {e}")
return []
def _create_dimension(self, p1, p2, direction, text):
"""创建尺寸标注"""
try:
if not BLENDER_AVAILABLE:
return None
# 在Blender中创建尺寸标注的简化实现
# 创建文本对象显示尺寸
midpoint = (
(p1[0] + p2[0]) / 2 + direction[0] * 0.1,
(p1[1] + p2[1]) / 2 + direction[1] * 0.1,
(p1[2] + p2[2]) / 2 + direction[2] * 0.1
)
dimension_obj = self._create_text_label(text, midpoint, direction)
# 创建尺寸线
if dimension_obj:
# 添加线条表示尺寸
line_mesh = bpy.data.meshes.new("DimensionLine")
vertices = [p1, p2]
edges = [(0, 1)]
line_mesh.from_pydata(vertices, edges, [])
line_mesh.update()
line_obj = bpy.data.objects.new("DimensionLine", line_mesh)
line_obj.parent = dimension_obj
bpy.context.scene.collection.objects.link(line_obj)
memory_manager.register_mesh(line_mesh)
memory_manager.register_object(line_obj)
return dimension_obj
except Exception as e:
logger.error(f"创建尺寸标注失败: {e}")
return None
# ==================== 几何体创建的辅助方法(补充) ====================
def _create_triangle_face(self, container, tri, offset_vec, base_point):
"""创建三角形面"""
try:
if not BLENDER_AVAILABLE:
return None
# 计算三角形的三个顶点
p1 = (tri.x, tri.y, tri.z)
p2 = (tri.x + offset_vec.x, tri.y +
offset_vec.y, tri.z + offset_vec.z)
p3 = (base_point.x + (base_point.x - tri.x),
base_point.y + (base_point.y - tri.y),
base_point.z + (base_point.z - tri.z))
# 创建网格
mesh = bpy.data.meshes.new("Triangle_Face")
vertices = [p1, p2, p3]
faces = [(0, 1, 2)]
mesh.from_pydata(vertices, [], faces)
mesh.update()
# 创建对象
obj = bpy.data.objects.new("Triangle_Face_Obj", mesh)
obj.parent = container
bpy.context.scene.collection.objects.link(obj)
memory_manager.register_mesh(mesh)
memory_manager.register_object(obj)
return obj
except Exception as e:
logger.error(f"创建三角形面失败: {e}")
return None
def _create_circle_face(self, container, center, normal, radius):
"""创建圆形面"""
try:
if not BLENDER_AVAILABLE:
return None
# 创建圆形网格
mesh = bpy.data.meshes.new("Circle_Face")
# 生成圆形顶点
segments = 32
vertices = [(center.x, center.y, center.z)] # 中心点
for i in range(segments):
angle = (i / segments) * 2 * math.pi
x = center.x + radius * math.cos(angle)
y = center.y + radius * math.sin(angle)
z = center.z
vertices.append((x, y, z))
# 创建面
faces = []
for i in range(segments):
next_i = (i + 1) % segments
faces.append((0, i + 1, next_i + 1))
mesh.from_pydata(vertices, [], faces)
mesh.update()
# 创建对象
obj = bpy.data.objects.new("Circle_Face_Obj", mesh)
obj.parent = container
bpy.context.scene.collection.objects.link(obj)
memory_manager.register_mesh(mesh)
memory_manager.register_object(obj)
return obj
except Exception as e:
logger.error(f"创建圆形面失败: {e}")
return None
def _apply_material_to_face(self, face, material):
"""为面应用材质"""
try:
if not face or not material or not BLENDER_AVAILABLE:
return
if hasattr(face, 'data') and face.data:
if not face.data.materials:
face.data.materials.append(material)
else:
face.data.materials[0] = material
except Exception as e:
logger.error(f"为面应用材质失败: {e}")
def _follow_me_face(self, face, path):
"""面跟随路径"""
try:
if not face or not path or not BLENDER_AVAILABLE:
return
# 在Blender中实现跟随路径
# 这里使用简化的实现
if hasattr(face, 'modifiers'):
# 添加阵列修改器或其他相关修改器
pass
except Exception as e:
logger.error(f"面跟随路径失败: {e}")
def _cleanup_path(self, path):
"""清理路径"""
try:
if path and BLENDER_AVAILABLE and path.name in bpy.data.objects:
bpy.data.objects.remove(path, do_unlink=True)
except Exception as e:
logger.error(f"清理路径失败: {e}")
def _cleanup_trimmer(self, trimmer):
"""清理修剪器"""
try:
if trimmer and BLENDER_AVAILABLE and trimmer.name in bpy.data.objects:
bpy.data.objects.remove(trimmer, do_unlink=True)
except Exception as e:
logger.error(f"清理修剪器失败: {e}")
def _trim_object(self, trimmer, target):
"""修剪对象"""
try:
if not trimmer or not target or not BLENDER_AVAILABLE:
return target
# 在Blender中实现布尔运算
# 这里使用简化的实现
return target
except Exception as e:
logger.error(f"修剪对象失败: {e}")
return target
def _mark_differ_faces(self, obj):
"""标记差异面"""
try:
if not obj or not BLENDER_AVAILABLE:
return
texture = self.get_texture("mat_default")
if not texture:
return
# 标记所有使用默认材质的面为差异面
for child in obj.children:
if hasattr(child, 'data') and child.data:
if (child.data.materials and
child.data.materials[0] == texture):
child["sw_differ"] = True
except Exception as e:
logger.error(f"标记差异面失败: {e}")
# ==================== 几何验证辅助方法 ====================
def _should_reverse_face(self, face, zaxis, reverse_face):
"""检查是否应该反转面"""
try:
if not face or not zaxis:
return False
# 简化的实现
return reverse_face
except Exception as e:
logger.error(f"检查面反转失败: {e}")
return False
def _face_normal_matches(self, face, zaxis):
"""检查面法向量是否匹配"""
try:
if not face or not zaxis:
return False
# 简化的实现
return True
except Exception as e:
logger.error(f"检查面法向量失败: {e}")
return False
def _reverse_face(self, face):
"""反转面"""
try:
if not face or not BLENDER_AVAILABLE:
return
if hasattr(face, 'data') and face.data:
# 在Blender中反转面的法向量
bpy.context.view_layer.objects.active = face
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.flip_normals()
bpy.ops.object.mode_set(mode='OBJECT')
except Exception as e:
logger.error(f"反转面失败: {e}")
def _get_face_normal(self, face):
"""获取面法向量"""
try:
if not face or not BLENDER_AVAILABLE:
return (0, 0, 1)
if hasattr(face, 'data') and face.data and face.data.polygons:
# 获取第一个多边形的法向量
return face.data.polygons[0].normal
return (0, 0, 1)
except Exception as e:
logger.error(f"获取面法向量失败: {e}")
return (0, 0, 1)
def _apply_follow_me(self, face, path):
"""应用跟随路径"""
try:
if not face or not path or not BLENDER_AVAILABLE:
return
# 在Blender中实现跟随路径的简化版本
# 这里需要根据实际需求实现具体的几何操作
pass
except Exception as e:
logger.error(f"应用跟随路径失败: {e}")
def _hide_edges(self, container):
"""隐藏边"""
try:
if not container or not BLENDER_AVAILABLE:
return
for child in container.children:
if hasattr(child, 'data') and child.data and hasattr(child.data, 'edges'):
for edge in child.data.edges:
edge.use_edge_sharp = True
except Exception as e:
logger.error(f"隐藏边失败: {e}")
def _create_face_fast(self, container, surface, material):
"""创建面 - 快速版本"""
try:
# 获取分段数据
segs = surface.get("segs", [])
if not segs:
return None
# 快速解析顶点
vertices = []
for seg in segs:
if len(seg) >= 2:
coord_str = seg[0].strip('()')
try:
x, y, z = map(float, coord_str.split(','))
vertices.append((x * 0.001, y * 0.001, z * 0.001))
except:
continue
if len(vertices) < 3:
return None
# 创建简单网格
mesh = bpy.data.meshes.new(f"FastFace_{int(time.time())}")
# 创建面(只支持三角形和四边形)
if len(vertices) == 4:
faces = [(0, 1, 2, 3)]
elif len(vertices) == 3:
faces = [(0, 1, 2)]
else:
# 复杂多边形简化为第一个三角形
faces = [(0, 1, 2)]
vertices = vertices[:3]
mesh.from_pydata(vertices, [], faces)
mesh.update()
# 创建对象
face_obj = bpy.data.objects.new(f"Face_{container.name}", mesh)
face_obj.parent = container
bpy.context.scene.collection.objects.link(face_obj)
# 应用材质
if material:
face_obj.data.materials.append(material)
# 确保可见
face_obj.hide_viewport = False
# 注册到内存管理器
memory_manager.register_mesh(mesh)
memory_manager.register_object(face_obj)
return face_obj
except Exception as e:
logger.error(f"快速创建面失败: {e}")
return None
def _create_board_six_faces_fast(self, leaf, data, color, scale, angle, color2, scale2, angle2):
"""快速创建板件六个面"""
try:
# 获取正反面数据
obv = data.get("obv") # 正面
rev = data.get("rev") # 反面
if not obv or not rev:
logger.warning("缺少正反面数据")
return
# 处理材质
antiz = data.get("antiz", False)
# 根据antiz决定材质分配
if antiz:
# 交换正反面材质
obv_color = color2 if color2 else color
rev_color = color
else:
# 正常材质分配
obv_color = color
rev_color = color2 if color2 else color
# 获取材质
material_obv = self.get_texture(obv_color) if obv_color else None
material_rev = self.get_texture(rev_color) if rev_color else None
edge_material = material_obv # 边面使用正面材质
# 1. 创建正面 (obverse)
obv_face = self._create_face_fast(leaf, obv, material_obv)
if obv_face:
obv_face["sw_face_type"] = "obverse"
obv_face["sw_face_id"] = "front"
obv_face["sw_ckey"] = obv_color
logger.debug("正面创建成功")
# 2. 创建反面 (reverse)
rev_face = self._create_face_fast(leaf, rev, material_rev)
if rev_face:
rev_face["sw_face_type"] = "reverse"
rev_face["sw_face_id"] = "back"
rev_face["sw_ckey"] = rev_color
logger.debug("反面创建成功")
# 3. 创建四个边面
self._create_board_edge_faces_fast(leaf, obv, rev, edge_material)
logger.debug("板件六面创建完成")
except Exception as e:
logger.error(f"创建板件六面失败: {e}")
def _create_board_edge_faces_fast(self, leaf, obv, rev, edge_material):
"""快速创建板件的四个边面"""
try:
# 解析正面和反面的顶点
obv_vertices = self._parse_surface_vertices(obv)
rev_vertices = self._parse_surface_vertices(rev)
if len(obv_vertices) != len(rev_vertices) or len(obv_vertices) < 3:
logger.warning("正反面顶点数量不匹配或不足")
return
# 创建四个边面
vertex_count = len(obv_vertices)
edge_count = 0
for i in range(vertex_count):
next_i = (i + 1) % vertex_count
# 边面的四个顶点:正面两个点 + 反面对应两个点
edge_vertices = [
obv_vertices[i], # 正面当前点
obv_vertices[next_i], # 正面下一点
rev_vertices[next_i], # 反面下一点
rev_vertices[i] # 反面当前点
]
# 创建边面
edge_face = self._create_face_from_vertices_fast(
leaf, edge_vertices, edge_material, f"edge_{i}")
if edge_face:
edge_face["sw_face_type"] = "edge"
edge_face["sw_face_id"] = f"edge_{i}"
edge_face["sw_edge_index"] = i
edge_count += 1
logger.debug(f"创建了 {edge_count}/{vertex_count} 个边面")
except Exception as e:
logger.error(f"创建边面失败: {e}")
def _parse_surface_vertices(self, surface):
"""解析表面顶点坐标"""
try:
vertices = []
segs = surface.get("segs", [])
for seg in segs:
if len(seg) >= 2:
coord_str = seg[0].strip('()')
try:
x, y, z = map(float, coord_str.split(','))
# 转换为米Blender使用米作为单位
vertices.append((x * 0.001, y * 0.001, z * 0.001))
except ValueError:
continue
return vertices
except Exception as e:
logger.error(f"解析表面顶点失败: {e}")
return []
def _create_face_from_vertices_fast(self, container, vertices, material, face_name):
"""从顶点快速创建面"""
try:
if len(vertices) < 3:
return None
# 创建网格
mesh = bpy.data.meshes.new(f"Face_{face_name}_{int(time.time())}")
# 创建面
if len(vertices) == 4:
faces = [(0, 1, 2, 3)]
elif len(vertices) == 3:
faces = [(0, 1, 2)]
else:
# 复杂多边形创建扇形三角形
faces = []
for i in range(1, len(vertices) - 1):
faces.append((0, i, i + 1))
mesh.from_pydata(vertices, [], faces)
mesh.update()
# 创建对象
face_obj = bpy.data.objects.new(f"Face_{face_name}", mesh)
face_obj.parent = container
bpy.context.scene.collection.objects.link(face_obj)
# 应用材质
if material:
face_obj.data.materials.append(material)
# 确保可见
face_obj.hide_viewport = False
# 注册到内存管理器
memory_manager.register_mesh(mesh)
memory_manager.register_object(face_obj)
return face_obj
except Exception as e:
logger.error(f"从顶点创建面失败: {e}")
return None
def _add_part_board_fast(self, part, data):
"""创建板材部件 - 保持六面逻辑的快速版本"""
try:
# 创建叶子组
leaf = bpy.data.objects.new(
f"Board_{part.name}_{int(time.time())}", None)
leaf.parent = part
bpy.context.scene.collection.objects.link(leaf)
# 获取材质信息
color = data.get("ckey", "mat_default")
scale = data.get("scale")
angle = data.get("angle")
color2 = data.get("ckey2")
scale2 = data.get("scale2")
angle2 = data.get("angle2")
# 设置叶子属性
leaf["sw_ckey"] = color
if scale:
leaf["sw_scale"] = scale
if angle:
leaf["sw_angle"] = angle
logger.debug(f"板材材质: {color}")
# 创建板件的六个面(正面、反面、四个边面)
self._create_board_six_faces_fast(
leaf, data, color, scale, angle, color2, scale2, angle2)
logger.debug(f"板材部件创建完成: {leaf.name}")
return leaf
except Exception as e:
logger.error(f"创建板材部件失败: {e}")
return None
def _create_transparent_material(self):
"""创建透明材质用于容器对象 - 修复依赖图问题"""
try:
material_name = "SUW_Container_Transparent"
# 检查是否已存在
if material_name in bpy.data.materials:
return bpy.data.materials[material_name]
# 创建透明材质
material = bpy.data.materials.new(name=material_name)
material.use_nodes = True
# 【修复】确保节点树存在
if not material.node_tree:
logger.error("材质节点树创建失败")
return None
# 清理默认节点
material.node_tree.nodes.clear()
# 【修复】创建节点时确保正确的依赖关系
try:
# 创建 Principled BSDF 节点
bsdf = material.node_tree.nodes.new(type='ShaderNodeBsdfPrincipled')
bsdf.location = (0, 0)
# 创建输出节点
output = material.node_tree.nodes.new(type='ShaderNodeOutputMaterial')
output.location = (300, 0)
# 【修复】确保节点有效后再连接
if bsdf and output and bsdf.outputs and output.inputs:
# 连接节点
material.node_tree.links.new(
bsdf.outputs['BSDF'], output.inputs['Surface'])
# 设置完全透明
if 'Base Color' in bsdf.inputs:
bsdf.inputs['Base Color'].default_value = (0.5, 0.5, 0.5, 1.0) # 灰色
if 'Alpha' in bsdf.inputs:
bsdf.inputs['Alpha'].default_value = 0.0 # 完全透明
# 设置混合模式
material.blend_method = 'BLEND'
material.use_backface_culling = False
# 【修复】强制更新材质节点
material.node_tree.update_tag()
logger.info(f"✅ 创建容器透明材质: {material_name}")
return material
else:
logger.error("节点创建失败,无法建立连接")
bpy.data.materials.remove(material)
return None
except Exception as e:
logger.error(f"材质节点设置失败: {e}")
bpy.data.materials.remove(material)
return None
except Exception as e:
logger.error(f"创建透明材质失败: {e}")
return None
def c03(self, data: Dict[str, Any]):
"""add_zone - 添加区域 - 异步安全版本,批量依赖图更新"""
# 此函数已由 _schedule_command 在主线程中安全调用,
# 无需再使用任何旧的 execute_in_main_thread 封装。
try:
uid = data.get("uid")
zid = data.get("zid")
# 约定zip_id 为 0 或 1 代表根级别,不寻找父级
zip_id = data.get("zip", 0)
elements = data.get("children", [])
# 创建区域组 - 保持为Empty对象
group_name = f"Zone_{zid}"
# 【修复】安全删除已存在的Zone - 避免递归导致假死
if BLENDER_AVAILABLE and group_name in bpy.data.objects:
old_group = bpy.data.objects[group_name]
# 【彻底重构】极简化的对象删除策略 - 避免复杂的清理逻辑
def delete_hierarchy_ultra_safe(root_obj):
"""超级安全的对象删除 - 最小化依赖图干扰"""
try:
# 【策略1】只删除对象让Blender自动处理网格清理
if root_obj and hasattr(root_obj, 'name') and root_obj.name in bpy.data.objects:
print(f"🗑️ 简单删除对象: {root_obj.name}")
# 【修复】使用非阻塞的选择清除
try:
if hasattr(root_obj, 'select_set'):
root_obj.select_set(False)
except:
pass
# 直接删除对象让Blender处理所有清理
bpy.data.objects.remove(root_obj, do_unlink=True)
# 从内存管理器中移除(但不强制清理网格)
if hasattr(root_obj, 'name'):
memory_manager.tracked_objects.discard(root_obj.name)
print(f"✅ 对象删除完成: {getattr(root_obj, 'name', 'Unknown')}")
else:
print(f"⚠️ 对象已不存在,跳过删除")
except Exception as e:
print(f"⚠️ 对象删除失败: {e}")
# 不进行任何额外的清理尝试,避免连锁问题
delete_hierarchy_ultra_safe(old_group)
print(f"✅ 安全删除了已存在的Zone: {group_name}")
if BLENDER_AVAILABLE:
group = bpy.data.objects.new(group_name, None)
else:
# 存根模式下创建模拟对象
class MockObject:
def __init__(self, name):
self.name = name
self.parent = None
self.children = []
self._props = {}
def __setitem__(self, key, value):
self._props[key] = value
def __getitem__(self, key):
return self._props.get(key)
group = MockObject(group_name)
# 核心逻辑修改:只有当 zip_id > 1 时,才代表一个真实的父对象
if zip_id > 1:
parent_zone_name = f"Zone_{zip_id}"
if BLENDER_AVAILABLE:
parent_zone = bpy.data.objects.get(parent_zone_name)
if parent_zone:
group.parent = parent_zone
else:
# 这个警告是正常的,如果父级命令还没到
print(f"⚠️ 未找到父Zone '{parent_zone_name}',对象 '{group_name}' 将被创建在根级别。")
else:
print(f"📝 存根模式设置父Zone '{parent_zone_name}' -> '{group_name}'")
# 设置自定义属性
group["sw_uid"] = uid
group["sw_zid"] = zid
group["sw_zip"] = zip_id
group["sw_typ"] = "zid"
if BLENDER_AVAILABLE:
bpy.context.scene.collection.objects.link(group)
else:
print(f"📝 存根模式:将对象 '{group_name}' 添加到场景")
# 存储引用 - 先维持现状,后续可优化为存储名称
if uid not in self.zones:
self.zones[uid] = {}
self.zones[uid][zid] = group
# 【修复】批量处理子元素,避免频繁的依赖图更新
if elements:
print(f"📦 开始创建 {len(elements)} 个子面...")
created_faces = []
# 批量创建所有子面,不立即更新依赖图
for i, element in enumerate(elements):
surf = element.get("surf")
if surf:
try:
face = self.create_face_safe(group, surf, transparent=True)
if face:
created_faces.append(face)
print(f"✅ 子面 {i+1}/{len(elements)} 创建成功: {face.name}")
else:
print(f"⚠️ 子面 {i+1}/{len(elements)} 创建失败")
except Exception as e:
print(f"💥 子面 {i+1}/{len(elements)} 创建异常: {e}")
continue
# 【临时禁用】依赖图批量更新 - 让Blender自动处理
if created_faces:
print(f"🔄 跳过依赖图更新让Blender自动处理")
# dependency_manager.request_update(force=True) # 暂时禁用
print(f"✅ 依赖图更新策略:自动模式")
print(f"🎉 Zone_{zid} 创建完成,包含 {len(created_faces)} 个子面")
return group
except Exception as e:
print(f"💥 c03命令执行时发生严重错误: {e}")
import traceback
traceback.print_exc()
return None
def create_face_safe(self, container, surface, color=None, scale=None, angle=None,
series=None, reverse_face=False, back_material=True,
saved_color=None, typ=None, transparent=False):
"""创建面 - 支持透明材质选项,修复依赖图和内存问题"""
try:
if not BLENDER_AVAILABLE:
logger.error("Blender不可用")
return None
# 获取分段数据
segs = surface.get("segs", [])
if not segs:
logger.error("没有分段数据")
return None
# 创建顶点
vertices = []
for i, seg in enumerate(segs):
if len(seg) >= 2:
coord_str = seg[0].strip('()')
try:
x, y, z = map(float, coord_str.split(','))
# 转换为米Blender使用米作为单位
vertex = (x * 0.001, y * 0.001, z * 0.001)
vertices.append(vertex)
except ValueError as e:
logger.error(f"解析顶点失败: {coord_str}, 错误: {e}")
continue
if len(vertices) < 3:
logger.error(f"顶点数量不足,无法创建面: {len(vertices)}")
return None
# 【修复】创建唯一的网格名称避免ID冲突
timestamp = int(time.time() * 1000000) # 微秒级时间戳
face_id = surface.get('f', 0)
mesh_name = f"SUW_Face_Mesh_{face_id}_{timestamp}"
obj_name = f"Face_{face_id}"
# 确保名称唯一性
counter = 1
original_obj_name = obj_name
while obj_name in bpy.data.objects:
obj_name = f"{original_obj_name}_{counter}"
counter += 1
# 【修复】使用上下文管理器确保安全的对象创建
mesh = None
face_obj = None
try:
# 创建网格
mesh = bpy.data.meshes.new(mesh_name)
# 创建面
edges = []
faces = []
if len(vertices) == 4:
# 四边形
faces = [(0, 1, 2, 3)]
elif len(vertices) == 3:
# 三角形
faces = [(0, 1, 2)]
else:
# 复杂多边形,创建扇形三角形
for i in range(1, len(vertices) - 1):
faces.append((0, i, i + 1))
# 【修复】安全的网格创建流程
mesh.from_pydata(vertices, edges, faces)
mesh.validate() # 验证网格数据
mesh.update() # 更新网格
# 【修复】创建对象前确保网格完整性
if not mesh.vertices or not mesh.polygons:
logger.error(f"网格创建失败,顶点数: {len(mesh.vertices)}, 面数: {len(mesh.polygons)}")
# 让Blender自动清理无效网格不手动删除
return None
# 创建对象
face_obj = bpy.data.objects.new(obj_name, mesh)
# 【修复】设置父对象关系 - 在添加到场景之前
if container and hasattr(container, 'name') and container.name in bpy.data.objects:
face_obj.parent = container
# 【修复】添加到场景 - 必须在设置属性之前
bpy.context.scene.collection.objects.link(face_obj)
# 设置面属性
if surface.get("p"):
face_obj["sw_p"] = surface["p"]
if surface.get("f"):
face_obj["sw_f"] = surface["f"]
# 确保对象可见
face_obj.hide_viewport = False
face_obj.hide_render = False
face_obj.hide_set(False)
# 【修复】材质应用 - 简化流程避免依赖图冲突
try:
if transparent:
# 应用透明材质
transparent_material = self._create_transparent_material()
if transparent_material:
# 确保材质槽存在
if not face_obj.data.materials:
face_obj.data.materials.append(None)
face_obj.data.materials[0] = transparent_material
except Exception as e:
logger.warning(f"材质应用失败: {e}")
# 【暂时禁用】内存管理器注册 - 让Blender自动管理
# if mesh:
# memory_manager.register_mesh(mesh)
# if face_obj:
# memory_manager.register_object(face_obj)
print(f"📝 对象创建完成使用Blender自动内存管理")
# 添加到series如果提供
if series is not None:
series.append(face_obj)
return face_obj
except Exception as e:
logger.error(f"对象创建过程中发生错误: {e}")
# 【修复】安全清理失败的对象
if face_obj:
try:
if hasattr(face_obj, 'name') and face_obj.name in bpy.data.objects:
bpy.data.objects.remove(face_obj, do_unlink=True)
if hasattr(face_obj, 'name'):
memory_manager.tracked_objects.discard(face_obj.name)
except (AttributeError, ReferenceError, RuntimeError):
# 对象可能已经被删除
pass
except Exception as e:
logger.debug(f"清理失败对象时的预期错误: {e}")
if mesh:
try:
# 只清理跟踪记录不删除网格数据让Blender自动处理
if hasattr(mesh, 'name'):
memory_manager.tracked_meshes.discard(mesh.name)
except (AttributeError, ReferenceError, RuntimeError):
# 网格可能已经被删除
pass
except Exception as e:
logger.debug(f"清理失败网格时的预期错误: {e}")
return None
except Exception as e:
logger.error(f"创建面失败: {e}")
import traceback
traceback.print_exc()
return None
def _execute_c16(self, data):
"""sel_zone - 选择区域"""
try:
return self.sel_zone_local(data)
except Exception as e:
print(f"💥 c16命令执行时发生严重错误: {e}")
import traceback
traceback.print_exc()
return None
def _execute_c17(self, data):
"""sel_elem - 选择元素(可能涉及父子关系设置)"""
try:
if self.part_mode:
return self.sel_part_parent(data)
else:
return self.sel_zone_local(data)
except Exception as e:
print(f"💥 c17命令执行时发生严重错误: {e}")
import traceback
traceback.print_exc()
return None
def sel_zone_local(self, data):
"""选择区域 - 本地执行"""
try:
uid = data.get("uid")
zid = data.get("zid")
print(f"🎯 选择区域: uid={uid}, zid={zid}")
# 清除之前的选择
self.sel_clear()
# 根据 uid 获取对应的 zones
if uid not in self.zones:
print(f"⚠️ 未找到 uid '{uid}' 对应的区域")
return None
zones = self.zones[uid]
# 查找指定的区域
if zid not in zones:
print(f"⚠️ 未找到 zid '{zid}' 对应的区域")
return None
zone = zones[zid]
# 这里可以添加选择逻辑,比如高亮显示等
print(f"✅ 成功选择区域: Zone_{zid}")
return zone
except Exception as e:
print(f"💥 sel_zone_local 执行失败: {e}")
return None
def sel_part_parent(self, data):
"""选择部件父级 - 可能涉及父子关系设置"""
try:
uid = data.get("uid")
zid = data.get("zid")
pid = data.get("pid")
print(f"🎯 选择部件父级: uid={uid}, zid={zid}, pid={pid}")
# 清除之前的选择
self.sel_clear()
# 这里可以添加部件父子关系的设置逻辑
# 根据你的具体需求来实现
print(f"✅ 成功选择部件父级")
return True
except Exception as e:
print(f"💥 sel_part_parent 执行失败: {e}")
return None
def sel_clear(self):
"""清除所有选择"""
try:
print("🔄 清除所有选择状态")
# 这里可以添加清除选择状态的逻辑
except Exception as e:
print(f"💥 sel_clear 执行失败: {e}")
# ==================== 模块级别的便利函数 ====================
def create_suw_instance():
"""创建SUW实例的便利函数"""
return SUWImpl.get_instance()
def cleanup_blender_memory():
"""清理Blender内存的便利函数"""
try:
if BLENDER_AVAILABLE:
cleanup_count = memory_manager.cleanup_orphaned_data()
gc.collect()
logger.info(f"清理了 {cleanup_count} 个孤立数据")
return cleanup_count
return 0
except Exception as e:
logger.error(f"清理内存失败: {e}")
return 0
def get_memory_usage_summary():
"""获取内存使用摘要"""
try:
if BLENDER_AVAILABLE:
return {
"objects": len(bpy.data.objects),
"meshes": len(bpy.data.meshes),
"materials": len(bpy.data.materials),
"memory_manager_stats": memory_manager.creation_stats.copy()
}
return {"blender_available": False}
except Exception as e:
logger.error(f"获取内存使用摘要失败: {e}")
return {"error": str(e)}
# ==================== 主程序入口 ====================
if __name__ == "__main__":
# 测试代码
try:
logger.info("开始测试SUWImpl内存管理")
# 创建实例
suw = create_suw_instance()
suw.startup()
# 获取内存报告
report = suw.get_memory_report()
logger.info(f"内存报告: {report}")
# 执行诊断
issues = suw.diagnose_system_state()
if not issues:
logger.info("✅ 系统状态正常")
# 清理测试
cleanup_count = cleanup_blender_memory()
logger.info(f"清理测试完成: {cleanup_count}")
except Exception as e:
logger.error(f"测试失败: {e}")
finally:
logger.info("测试完成")