suwoodblender/blenderpython/suw_impl - 副本 (23).py

5364 lines
188 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):
"""清理孤立数据 - 修复版本"""
if not BLENDER_AVAILABLE:
return
# 确保在主线程中执行
if threading.current_thread() != threading.main_thread():
return
try:
with self._cleanup_lock:
current_time = time.time()
if current_time - self.last_cleanup < 30: # 30秒内不重复清理
return
logger.debug("🧹 开始清理孤立数据...")
cleanup_count = {
'meshes': 0,
'materials': 0,
'images': 0,
'collections': 0,
'objects': 0
}
# 清理孤立的网格
orphaned_meshes = []
for mesh in bpy.data.meshes:
if mesh.users == 0:
orphaned_meshes.append(mesh)
for mesh in orphaned_meshes:
try:
bpy.data.meshes.remove(mesh)
cleanup_count['meshes'] += 1
except:
pass
# 清理孤立的材质
orphaned_materials = []
for material in bpy.data.materials:
if material.users == 0:
orphaned_materials.append(material)
for material in orphaned_materials:
try:
bpy.data.materials.remove(material)
cleanup_count['materials'] += 1
except:
pass
# 清理孤立的图像
orphaned_images = []
for image in bpy.data.images:
if image.users == 0 and not image.is_dirty:
orphaned_images.append(image)
for image in orphaned_images:
try:
bpy.data.images.remove(image)
cleanup_count['images'] += 1
except:
pass
# 清理孤立的集合
orphaned_collections = []
for collection in bpy.data.collections:
if collection.users == 0:
orphaned_collections.append(collection)
for collection in orphaned_collections:
try:
bpy.data.collections.remove(collection)
cleanup_count['collections'] += 1
except:
pass
# 清理跟踪集合中的无效引用
self._cleanup_tracked_references()
# 强制垃圾回收
gc.collect()
self.last_cleanup = current_time
self.operation_count = 0
# 只在有实际清理时才输出日志
total_cleaned = sum(cleanup_count.values())
if total_cleaned > 0:
logger.info(f"✅ 清理完成: {cleanup_count}")
except Exception as e:
logger.error(f"清理孤立数据失败: {e}")
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()
# 全局主线程任务队列
_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(func: Callable, *args, **kwargs) -> Any:
"""在主线程中执行函数 - 增加超时时间"""
global _main_thread_queue, _main_thread_id
# 如果已经在主线程中,直接执行
if threading.current_thread().ident == _main_thread_id:
return func(*args, **kwargs)
# 否则,将任务放入队列
result_queue = queue.Queue()
exception_queue = queue.Queue()
def wrapper():
try:
result = func(*args, **kwargs)
result_queue.put(result)
except Exception as e:
exception_queue.put(e)
_main_thread_queue.put(wrapper)
# 等待结果增加超时时间到60秒
timeout = 60
start_time = time.time()
while time.time() - start_time < timeout:
try:
if not exception_queue.empty():
raise exception_queue.get_nowait()
if not result_queue.empty():
return result_queue.get_nowait()
time.sleep(0.01)
except queue.Empty:
continue
raise TimeoutError("主线程执行超时")
def process_main_thread_tasks():
"""处理主线程任务队列 - 应该在主线程中定期调用"""
global _main_thread_queue
try:
while not _main_thread_queue.empty():
task = _main_thread_queue.get_nowait()
task()
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
logger.info("SUWImpl 初始化完成,启用内存管理优化")
@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 = execute_in_main_thread(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
def create_part():
try:
# 获取部件集合
parts = self.get_parts(data)
# 创建部件容器
part = bpy.data.objects.new(f"Part_{root}", None)
bpy.context.scene.collection.objects.link(part)
# 应用透明材质给Part容器
transparent_material = self._create_transparent_material()
if transparent_material:
# Part是Empty对象不能直接应用材质所以保持原样
pass
# 设置部件属性
part["sw_uid"] = uid
part["sw_cp"] = root
part["sw_typ"] = "part"
# 存储部件
parts[root] = part
memory_manager.register_object(part)
# 处理finals数据
finals = data.get("finals", [])
for final_data in finals:
try:
board = self._create_board_with_material_and_uv(
part, final_data)
if not board:
logger.warning("板材创建失败")
except Exception as e:
logger.error(f"创建板材失败: {e}")
return part
except Exception as e:
logger.error(f"创建部件失败: {e}")
return None
# 执行创建
part = execute_in_main_thread(create_part)
if not part:
logger.error(f"零件创建失败: uid={uid}, cp={root}")
except Exception as e:
logger.error(f"添加零件失败: {e}")
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) >= 4 and len(rev_vertices) >= 4:
# 计算板材尺寸
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)
# 创建立方体
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"
# 关联材质
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)
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"
# 关联材质
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)
# 获取材质信息
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}")
# 处理截面
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 = execute_in_main_thread(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]):
"""add_machining - 添加加工 - 线程安全版本"""
try:
if not BLENDER_AVAILABLE:
return
uid = data.get("uid")
def create_machining():
try:
works = data.get("works", [])
machinings = []
for work in works:
component = work.get("component")
machining = self._create_machining(component, work)
if machining:
machinings.append(machining)
# 存储加工
if uid not in self.machinings:
self.machinings[uid] = []
self.machinings[uid].extend(machinings)
for machining in machinings:
memory_manager.register_object(machining)
return len(machinings)
except Exception as e:
logger.error(f"创建加工失败: {e}")
return 0
# 在主线程中执行加工创建
count = execute_in_main_thread(create_machining)
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 _create_machining(self, component, work):
"""创建加工对象"""
try:
if not BLENDER_AVAILABLE:
return None
special = work.get("special", 0)
machining = bpy.data.objects.new("Machining", None)
machining.parent = component
bpy.context.scene.collection.objects.link(machining)
machining["sw_typ"] = "work"
machining["sw_special"] = special
# 解析点
p1 = Point3d.parse(work.get("p1", "(0,0,0)"))
p2 = Point3d.parse(work.get("p2", "(0,0,0)"))
# 创建路径和面
if "tri" in work:
# 三角形加工
tri = Point3d.parse(work.get("tri", "(0,0,0)"))
p3 = Point3d.parse(work.get("p3", "(0,0,0)"))
path = self._create_line_path(p1, p3)
face = self._create_triangle_face(machining, tri, p2, p1)
elif "surf" in work:
# 表面加工
path = self._create_line_path(p1, p2)
surf = work.get("surf", {})
face = self.create_face(machining, surf)
else:
# 圆形加工
path = self._create_line_path(p1, p2)
za = self._normalize_vector(
p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)
dia = work.get("dia", 6) * 0.001 # mm to meters
face = self._create_circle_face(machining, p1, za, dia / 2.0)
# 设置材质
if special == 0 and work.get("cancel", 0) == 0:
texture = self.get_texture("mat_machine")
if face and texture:
self._apply_material_to_face(face, texture)
# 执行跟随
if face and path:
self._follow_me_face(face, path)
self._cleanup_path(path)
return machining
except Exception as e:
logger.error(f"创建加工对象失败: {e}")
return None
def _work_trimmed(self, part, work):
"""工作修剪"""
try:
if not BLENDER_AVAILABLE:
return
# 获取所有子叶
leaves = []
for child in part.children:
if child.get("sw_typ") == "cp":
leaves.append(child)
for leaf in leaves:
if not leaf or leaf.name not in bpy.data.objects:
continue
# 保存属性
attributes = {}
for key in leaf.keys():
if key.startswith("sw_"):
attributes[key] = leaf[key]
# 创建修剪器
trimmer = self._create_trimmer(part, work)
if not trimmer:
continue
# 执行修剪
trimmed = self._trim_object(trimmer, leaf)
# 恢复属性
if trimmed:
for key, value in attributes.items():
trimmed[key] = value
# 清理
self._cleanup_trimmer(trimmer)
# 处理差异面
if work.get("differ", False):
self._mark_differ_faces(leaf)
except Exception as e:
logger.error(f"工作修剪失败: {e}")
def _create_trimmer(self, part, work):
"""创建修剪器"""
try:
trimmer = bpy.data.objects.new("Trimmer", None)
trimmer.parent = part
bpy.context.scene.collection.objects.link(trimmer)
p1 = Point3d.parse(work.get("p1", "(0,0,0)"))
p2 = Point3d.parse(work.get("p2", "(0,0,0)"))
if "tri" in work:
# 三角形修剪器
tri = Point3d.parse(work.get("tri", "(0,0,0)"))
p3 = Point3d.parse(work.get("p3", "(0,0,0)"))
path = self._create_line_path(p1, p3)
face = self._create_triangle_face(trimmer, tri, p2, p1)
elif "surf" in work:
# 表面修剪器
path = self._create_line_path(p1, p2)
surf = work.get("surf", {})
face = self.create_face(trimmer, surf)
else:
# 圆形修剪器
path = self._create_line_path(p1, p2)
za = self._normalize_vector(
p2.x - p1.x, p2.y - p1.y, p2.z - p1.z)
dia = work.get("dia", 6) * 0.001
face = self._create_circle_face(trimmer, p1, za, dia / 2.0)
# 设置差异材质
if work.get("differ", False):
texture = self.get_texture("mat_default")
if face and texture:
self._apply_material_to_face(face, texture)
# 执行跟随
if face and path:
self._follow_me_face(face, path)
return trimmer
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:
bpy.ops.object.select_all(action='DESELECT')
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 - 删除实体 - 修复对象引用问题"""
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":
# 删除整个单元的所有内容
self._del_unit_entities_safe(uid)
elif typ == "wall":
# 删除墙体实体
self._del_wall_entity_safe(data, uid, oid)
else:
# 删除标准实体
self._del_standard_entities_safe(data, uid, typ, oid)
# 清理标签
self._clear_labels_safe()
# 强制更新视图
bpy.context.view_layer.update()
logger.info(f"✅ 删除实体完成: uid={uid}, typ={typ}")
except Exception as e:
logger.error(f"删除实体失败: {e}")
import traceback
logger.error(traceback.format_exc())
# 在主线程中执行删除操作
execute_in_main_thread(delete_entities)
except Exception as e:
logger.error(f"❌ 删除实体失败: {e}")
def _del_unit_entities_safe(self, uid: str):
"""安全删除单元所有实体"""
try:
if not uid:
return
logger.info(f"删除单元所有实体: {uid}")
# 收集要删除的对象
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")
if obj_uid == uid:
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)} 个对象")
# 清理相关数据结构
self._cleanup_unit_data(uid)
except Exception as e:
logger.error(f"删除单元实体失败: {e}")
def _del_standard_entities_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 == "zid":
entities = self.get_zones(data)
elif typ == "cp":
entities = self.get_parts(data)
elif typ == "hw":
entities = self.get_hardwares(data)
if entities and oid in entities:
obj = entities[oid]
if self._delete_object_safe(obj):
del entities[oid]
logger.info(f"✅ 删除实体成功: {typ}={oid}")
else:
logger.warning(f"实体删除失败: {typ}={oid}")
else:
logger.warning(f"未找到实体: {typ}={oid}")
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 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}")
# 删除对象的网格数据(如果有)
mesh_data = None
if obj_type == 'MESH' and obj.data:
mesh_data = obj.data
# 删除对象
try:
bpy.data.objects.remove(obj, do_unlink=True)
logger.debug(f"删除对象成功: {obj_name}")
except Exception as e:
logger.error(f"删除对象失败: {obj_name}, 错误: {e}")
return False
# 删除网格数据
if mesh_data and mesh_data.name in bpy.data.meshes:
try:
bpy.data.meshes.remove(mesh_data, do_unlink=True)
logger.debug(f"删除网格数据成功: {mesh_data.name}")
except Exception as e:
logger.warning(f"删除网格数据失败: {e}")
return True
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 = execute_in_main_thread(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 = execute_in_main_thread(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:
self.sel_clear()
zones = self.get_zones(data)
for zid, zone in zones.items():
if zone and zone.name in bpy.data.objects:
leaf = self._is_leaf_zone(zid, zones)
zone.hide_viewport = leaf and self.hide_none
except Exception as e:
logger.error(f"选择单元失败: {e}")
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 c16(self, data: Dict[str, Any]):
"""sel_zone - 选择区域"""
try:
self._sel_zone_local(data)
except Exception as e:
logger.error(f"选择区域失败: {e}")
def c17(self, data: Dict[str, Any]):
"""sel_elem - 选择元素"""
try:
if self.part_mode:
self._sel_part_parent(data)
else:
self._sel_zone_local(data)
except Exception as e:
logger.error(f"选择元素失败: {e}")
def _sel_part_parent(self, data):
"""选择零件父级"""
try:
self.sel_clear()
zones = self.get_zones(data)
parts = self.get_parts(data)
hardwares = self.get_hardwares(data)
uid = data.get("uid")
zid = data.get("zid")
pid = data.get("pid")
parted = False
# 选择零件
for v_root, part in parts.items():
if part and part.get("sw_pid") == pid:
self.textured_part(part, True)
self.__class__._selected_uid = uid
self.__class__._selected_obj = pid
parted = True
# 选择硬件
for v_root, hw in hardwares.items():
if hw and hw.get("sw_pid") == pid:
self._textured_hw(hw, True)
# 处理区域
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
if leaf and child_id == zid:
if not self.hide_none:
child_zone.hide_viewport = False
for face in child_zone.children:
if hasattr(face, 'data'):
selected = face.get("sw_child") == pid
self._textured_face(face, selected)
if selected:
self.__class__._selected_uid = uid
self.__class__._selected_obj = pid
elif not leaf and child_id == zid:
if not parted:
child_zone.hide_viewport = False
for face in child_zone.children:
if (hasattr(face, 'data') and
face.get("sw_child") == pid):
self._textured_face(face, True)
self.__class__._selected_uid = uid
self.__class__._selected_obj = pid
elif leaf and not self.hide_none:
child_zone.hide_viewport = False
for face in child_zone.children:
if hasattr(face, 'data'):
self._textured_face(face, False)
else:
child_zone.hide_viewport = True
except Exception as e:
logger.error(f"选择零件父级失败: {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 = execute_in_main_thread(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 = execute_in_main_thread(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 = execute_in_main_thread(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 = execute_in_main_thread(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
# 清理默认节点
material.node_tree.nodes.clear()
# 创建节点
bsdf = material.node_tree.nodes.new(
type='ShaderNodeBsdfPrincipled')
bsdf.location = (0, 0)
output = material.node_tree.nodes.new(
type='ShaderNodeOutputMaterial')
output.location = (300, 0)
# 连接节点
material.node_tree.links.new(
bsdf.outputs['BSDF'], output.inputs['Surface'])
# 设置完全透明
bsdf.inputs['Base Color'].default_value = (
0.5, 0.5, 0.5, 1.0) # 灰色
bsdf.inputs['Alpha'].default_value = 0.0 # 完全透明
# 设置混合模式
material.blend_method = 'BLEND'
material.use_backface_culling = False
logger.info(f"✅ 创建容器透明材质: {material_name}")
return material
except Exception as e:
logger.error(f"创建透明材质失败: {e}")
return None
def c03(self, data: Dict[str, Any]):
"""add_zone - 添加区域 - 修复父对象设置问题"""
try:
if not BLENDER_AVAILABLE:
logger.error("Blender不可用")
return
uid = data.get("uid")
zid = data.get("zid")
zip_id = data.get("zip", -1)
elements = data.get("children", [])
logger.info(f"🏗️ 开始创建区域: uid={uid}, zid={zid}, zip={zip_id}")
def create_zone():
try:
# 创建区域组 - 保持为Empty对象
group_name = f"Zone_{zid}"
group = bpy.data.objects.new(group_name, None)
# 设置父对象 - 根据zip_id查找父Zone
if zip_id > 0:
# 查找父Zone
parent_zone_name = f"Zone_{zip_id}"
parent_zone = bpy.data.objects.get(parent_zone_name)
if parent_zone:
group.parent = parent_zone
else:
logger.warning(f"未找到父Zone: {parent_zone_name}")
# 如果zip_id <= 0则不设置父对象顶级Zone
# 设置属性
group["sw_uid"] = uid
group["sw_zid"] = zid
group["sw_zip"] = zip_id
group["sw_typ"] = "zid"
bpy.context.scene.collection.objects.link(group)
# 将Zone存储到zones字典中
zones_dict = self.get_zones(data)
zones_dict[zid] = group
# 处理子元素 - 给face应用透明材质
for element in elements:
surf = element.get("surf")
if surf:
self.create_face_safe(
group, surf, transparent=True)
logger.info(f"✅ 区域创建完成: {group_name}")
return group
except Exception as e:
logger.error(f"创建区域失败: {e}")
return None
# 在主线程执行
return execute_in_main_thread(create_zone)
except Exception as e:
logger.error(f"c03命令失败: {e}")
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
# 创建网格
mesh_name = f"Face_{surface.get('f', 0)}_{int(time.time())}"
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.update()
# 创建对象
obj_name = f"Face_{surface.get('f', 0)}"
face_obj = bpy.data.objects.new(obj_name, mesh)
# 设置父对象
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)
# 应用材质
if transparent:
# 应用透明材质
transparent_material = self._create_transparent_material()
if transparent_material:
face_obj.data.materials.append(transparent_material)
logger.info(f"✅ Face {obj_name} 应用透明材质")
elif color:
# 应用指定材质
material = self.get_texture(color)
if material:
face_obj.data.materials.append(material)
else:
# 创建默认材质
default_mat = bpy.data.materials.new(
name="DefaultMaterial")
default_mat.use_nodes = True
default_mat.node_tree.nodes["Principled BSDF"].inputs[0].default_value = (
0.8, 0.8, 0.8, 1.0)
face_obj.data.materials.append(default_mat)
# 注册到内存管理器
memory_manager.register_mesh(mesh)
memory_manager.register_object(face_obj)
# 添加到series如果提供
if series is not None:
series.append(face_obj)
return face_obj
except Exception as e:
logger.error(f"创建面失败: {e}")
return None
# ==================== 模块级别的便利函数 ====================
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("测试完成")