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