diff --git a/.gitignore b/.gitignore
index 74ae8b6c..aa6cef21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
__pycache__/
*.pyc
.idea/
-*.pyc
diff --git a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py
index 96b82c7a..1d8c480d 100644
--- a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py
+++ b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py
@@ -121,25 +121,43 @@ class InternalLightManager(object):
print("ERROR: Could not detach light, light was not attached!")
return
- self._lights.free_slot(light.get_slot())
+ # 保存必要信息,避免过早清理
+ light_slot = light.get_slot()
+ has_shadows = light.get_casts_shadows()
+
+ # 处理shadow sources(如果有的话)
+ if has_shadows:
+ num_sources = light.get_num_shadow_sources()
+ if num_sources > 0:
+ # 关键修复:先保存第一个source的slot,再清理
+ first_source = light.get_shadow_source(0)
+ first_source_slot = first_source.get_slot() # 保存slot值
+
+ # 先发送GPU移除命令(在清理之前)
+ cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources)
+ cmd_remove.push_int(first_source_slot) # 使用保存的slot值
+ cmd_remove.push_int(num_sources)
+ self._cmd_list.add_command(cmd_remove)
+
+ # 然后清理所有shadow sources
+ for i in range(num_sources):
+ source = light.get_shadow_source(i)
+ if source.has_slot():
+ self._shadow_sources.free_slot(source.get_slot())
+ if source.has_region():
+ self._shadow_manager.get_atlas().free_region(source.get_region())
+ source.clear_region()
+
+ # 清理light的shadow sources
+ light.clear_shadow_sources()
+
+ # 发送GPU移除light命令
self.gpu_remove_light(light)
+
+ # 最后释放light slot和清理light信息
+ self._lights.free_slot(light_slot)
light.remove_slot()
- if light.get_casts_shadows():
-
- for i in range(light.get_num_shadow_sources()):
- source = light.get_shadow_source(i)
- if source.has_slot():
- self._shadow_sources.free_slot(source.get_slot())
- if source.has_region():
- self._shadow_manager.get_atlas().free_region(source.get_region())
- source.clear_region()
-
- self.gpu_remove_consecutive_sources(
- light.get_shadow_source(0), light.get_num_shadow_sources())
-
- light.clear_shadow_sources()
-
def gpu_remove_consecutive_sources(self, first_source, num_sources):
cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources)
cmd_remove.push_int(first_source.get_slot())
diff --git a/Resources/icons/test_metallic_stripes.png b/Resources/icons/test_metallic_stripes.png
new file mode 100644
index 00000000..956c8661
Binary files /dev/null and b/Resources/icons/test_metallic_stripes.png differ
diff --git a/Resources/icons/test_roughness_checkerboard.png b/Resources/icons/test_roughness_checkerboard.png
new file mode 100644
index 00000000..3ee2bd65
Binary files /dev/null and b/Resources/icons/test_roughness_checkerboard.png differ
diff --git a/Resources/icons/test_roughness_circle.png b/Resources/icons/test_roughness_circle.png
new file mode 100644
index 00000000..5ab3d4d6
Binary files /dev/null and b/Resources/icons/test_roughness_circle.png differ
diff --git a/Resources/icons/test_roughness_gradient.png b/Resources/icons/test_roughness_gradient.png
new file mode 100644
index 00000000..11dbcc81
Binary files /dev/null and b/Resources/icons/test_roughness_gradient.png differ
diff --git a/core/collision_manager.py b/core/collision_manager.py
new file mode 100644
index 00000000..041571bf
--- /dev/null
+++ b/core/collision_manager.py
@@ -0,0 +1,945 @@
+import time
+from datetime import datetime
+from panda3d.core import (
+ CollisionTraverser, CollisionHandlerQueue, CollisionNode,
+ CollisionRay, CollisionSphere, BitMask32, Point3, Vec3
+)
+from direct.task.TaskManagerGlobal import taskMgr
+
+class CollisionManager:
+ """统一的碰撞检测管理器"""
+
+ def __init__(self, world):
+ self.world = world
+ self.traverser = CollisionTraverser("main_traverser")
+ self.queue = CollisionHandlerQueue()
+
+ # 碰撞掩码定义
+ self.MASKS = {
+ # === 基础碰撞掩码定义 ===
+ # 每个掩码使用不同的位(bit)来标识,可以进行位运算组合
+
+ 'TERRAIN': BitMask32.bit(0), # 地形/地面 - 通常用于地面碰撞检测
+ 'UI_ELEMENT': BitMask32.bit(1), # UI元素 - 界面组件的碰撞检测
+ 'CAMERA': BitMask32.bit(2), # 摄像机 - 相机的碰撞检测
+ 'MODEL_COLLISION': BitMask32.bit(3), # 模型碰撞 - 通用模型间碰撞检测
+ }
+
+ # 碰撞体形状类型
+ self.COLLISION_SHAPES = {
+ 'SPHERE': 'sphere',
+ 'BOX': 'box',
+ 'CAPSULE': 'capsule',
+ 'PLANE': 'plane',
+ 'POLYGON': 'polygon',
+ 'MESH': 'mesh'
+ }
+
+ # 掩码组合定义
+ self.MASK_COMBINATIONS = {
+ }
+
+ # 碰撞分组管理
+ self.collision_groups = {}
+ self.group_enabled = {}
+
+ # 碰撞回调系统
+ self.collision_callbacks = {}
+ self.collision_filters = {}
+
+ # 空间分割系统(八叉树)
+ self.spatial_partitioning_enabled = False
+ self.octree = None
+ self.octree_max_depth = 6
+ self.octree_max_objects = 10
+
+ # 连续碰撞检测
+ self.ccd_enabled = False
+ self.ccd_threshold = 10.0 # 速度阈值
+
+ # 性能监控
+ self.performance_monitor = CollisionPerformanceMonitor()
+
+ # 模型间碰撞检测配置
+ self.model_collision_enabled = False
+ self.collision_detection_frequency = 0.1
+ self.collision_distance_threshold = 0
+ self.last_collision_check = 0
+ self.collision_history = []
+ self.max_collision_history = 100
+
+ # 碰撞状态跟踪 - 避免重复打印
+ self.active_collisions = set() # 当前活跃的碰撞对
+
+ # 模型间碰撞检测任务
+ self.collision_task = None
+
+ print("✅ 碰撞检测管理器初始化完成")
+
+ def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
+ """启用/禁用模型间碰撞检测
+
+ Args:
+ enable: 是否启用碰撞检测
+ frequency: 检测频率(秒)
+ threshold: 碰撞距离阈值
+ """
+ self.model_collision_enabled = enable
+ self.collision_detection_frequency = frequency
+ self.collision_distance_threshold = threshold
+
+ if enable:
+ print(f"✅ 启用模型间碰撞检测 - 频率: {frequency}s, 阈值: {threshold}")
+ self._startCollisionDetectionTask()
+ else:
+ print("❌ 禁用模型间碰撞检测")
+ self._stopCollisionDetectionTask()
+
+ def _startCollisionDetectionTask(self):
+ """启动碰撞检测任务"""
+ if self.collision_task:
+ taskMgr.remove(self.collision_task)
+
+ def collisionDetectionTask(task):
+ try:
+ self.detectModelCollisions()
+ return task.again
+ except Exception as e:
+ print(f"❌ 碰撞检测任务异常: {str(e)}")
+ return task.again
+
+ self.collision_task = taskMgr.doMethodLater(
+ self.collision_detection_frequency,
+ collisionDetectionTask,
+ "modelCollisionDetection"
+ )
+
+ def _stopCollisionDetectionTask(self):
+ """停止碰撞检测任务"""
+ if self.collision_task:
+ taskMgr.remove(self.collision_task)
+ self.collision_task = None
+
+ def detectModelCollisions(self, specific_models=None, log_results=True):
+ """检测模型间碰撞"""
+ if not self.model_collision_enabled and specific_models is None:
+ return []
+
+ start_time = time.perf_counter()
+ current_time = datetime.now()
+
+ models_to_check = specific_models if specific_models else self.world.models
+ if len(models_to_check) < 2:
+ return []
+
+ collision_results = []
+ checked_pairs = set()
+ current_collision_pairs = set()
+
+ # 遍历所有模型对
+ for i, model_a in enumerate(models_to_check):
+ for j, model_b in enumerate(models_to_check[i+1:], i+1):
+ pair_key = tuple(sorted([id(model_a), id(model_b)]))
+ if pair_key in checked_pairs:
+ continue
+ checked_pairs.add(pair_key)
+
+ collision_info = self._checkModelPairCollision(model_a, model_b, current_time)
+ if collision_info:
+ collision_results.append(collision_info)
+ current_collision_pairs.add(pair_key)
+
+ # 只有新碰撞才打印日志
+ if log_results and pair_key not in self.active_collisions:
+ self._logCollisionInfo(collision_info)
+ print(f"🆕 新碰撞检测到!")
+
+ # 检测碰撞结束的情况
+ ended_collisions = self.active_collisions - current_collision_pairs
+ for pair_key in ended_collisions:
+ print(f"✅ 碰撞结束: 模型对 {pair_key}")
+
+ # 更新活跃碰撞状态
+ self.active_collisions = current_collision_pairs
+
+ # 记录性能和历史
+ detection_time = time.perf_counter() - start_time
+ self.performance_monitor.recordModelCollisionDetection(
+ detection_time, len(models_to_check), len(collision_results))
+
+ if collision_results:
+ self.collision_history.extend(collision_results)
+ if len(self.collision_history) > self.max_collision_history:
+ self.collision_history = self.collision_history[-self.max_collision_history:]
+
+ return collision_results
+
+ def _checkModelPairCollision(self, model_a, model_b, timestamp):
+ """使用Panda3D内置碰撞系统检查两个模型是否碰撞"""
+ try:
+ # 检查模型是否有碰撞节点
+ collision_node_a = self._findCollisionNode(model_a)
+ collision_node_b = self._findCollisionNode(model_b)
+
+ if not collision_node_a or not collision_node_b:
+ return None
+
+ # 使用Panda3D的碰撞遍历器
+ temp_traverser = CollisionTraverser()
+ temp_queue = CollisionHandlerQueue()
+
+ # 设置碰撞检测
+ temp_traverser.addCollider(collision_node_a, temp_queue)
+
+ # 执行碰撞检测
+ temp_traverser.traverse(model_b)
+
+ if temp_queue.getNumEntries() > 0:
+ # 有碰撞,获取碰撞信息
+ entry = temp_queue.getEntry(0)
+
+ center_a = model_a.getPos(self.world.render)
+ center_b = model_b.getPos(self.world.render)
+ distance = (center_b - center_a).length()
+
+ return {
+ 'timestamp': timestamp,
+ 'model_a': {
+ 'name': model_a.getName(),
+ 'center': center_a,
+ 'radius': 0, # 不再需要手动计算半径
+ 'node': model_a
+ },
+ 'model_b': {
+ 'name': model_b.getName(),
+ 'center': center_b,
+ 'radius': 0,
+ 'node': model_b
+ },
+ 'collision_point': entry.getSurfacePoint(self.world.render),
+ 'distance': distance,
+ 'penetration_depth': 0, # Panda3D会自动处理
+ 'collision_normal': entry.getSurfaceNormal(self.world.render)
+ }
+
+ return None
+
+ except Exception as e:
+ print(f"❌ 检测模型对碰撞失败 ({model_a.getName()} vs {model_b.getName()}): {str(e)}")
+ return None
+
+ def _findCollisionNode(self, model):
+ """查找模型的碰撞节点"""
+ for child in model.getChildren():
+ if isinstance(child.node(), CollisionNode):
+ return child
+ return None
+
+ def _logCollisionInfo(self, collision_info):
+ """输出碰撞信息日志"""
+ timestamp = collision_info['timestamp'].strftime('%H:%M:%S.%f')[:-3]
+ model_a = collision_info['model_a']
+ model_b = collision_info['model_b']
+
+ print(f"🔥 [{timestamp}] 检测到碰撞:")
+ print(f" 模型A: {model_a['name']} (中心: {model_a['center']}, 半径: {model_a['radius']:.2f})")
+ print(f" 模型B: {model_b['name']} (中心: {model_b['center']}, 半径: {model_b['radius']:.2f})")
+ print(f" 碰撞点: {collision_info['collision_point']}")
+ print(f" 中心距离: {collision_info['distance']:.3f}")
+ print(f" 穿透深度: {collision_info['penetration_depth']:.3f}")
+ print(f" 碰撞法向: {collision_info['collision_normal']}")
+
+ def getCollisionHistory(self, limit=None):
+ """获取碰撞历史记录
+
+ Args:
+ limit: 返回记录数量限制
+
+ Returns:
+ list: 碰撞历史记录
+ """
+ if limit:
+ return self.collision_history[-limit:]
+ return self.collision_history.copy()
+
+ def clearCollisionHistory(self):
+ """清除碰撞历史记录"""
+ self.collision_history.clear()
+ print("✅ 碰撞历史记录已清除")
+
+ def getCollisionStatistics(self):
+ """获取碰撞统计信息"""
+ if not self.collision_history:
+ return {"total_collisions": 0, "unique_pairs": 0, "most_frequent_pair": None}
+
+ # 统计碰撞对
+ collision_pairs = {}
+ for collision in self.collision_history:
+ pair_key = tuple(sorted([
+ collision['model_a']['name'],
+ collision['model_b']['name']
+ ]))
+ collision_pairs[pair_key] = collision_pairs.get(pair_key, 0) + 1
+
+ most_frequent_pair = max(collision_pairs.items(), key=lambda x: x[1])
+
+ return {
+ "total_collisions": len(self.collision_history),
+ "unique_pairs": len(collision_pairs),
+ "most_frequent_pair": {
+ "models": most_frequent_pair[0],
+ "count": most_frequent_pair[1]
+ },
+ "collision_pairs": collision_pairs
+ }
+
+ def createCollisionShape(self, model, shape_type='auto', **kwargs):
+ """为模型创建指定类型的碰撞体
+
+ Args:
+ model: 模型节点
+ shape_type: 碰撞体类型 ('auto', 'sphere', 'box', 'capsule', 'plane', 'polygon')
+ **kwargs: 形状特定参数
+
+ Returns:
+ CollisionSolid: 创建的碰撞体
+ """
+ from panda3d.core import (
+ CollisionSphere, CollisionBox, CollisionCapsule,
+ CollisionPlane, CollisionPolygon, Plane, Vec3
+ )
+
+ bounds = model.getBounds()
+ if bounds.isEmpty():
+ # 默认小球体
+ return CollisionSphere(Point3(0, 0, 0), 1.0)
+
+ center = bounds.getCenter()
+ radius = bounds.getRadius()
+
+ # 自动选择最适合的形状
+ if shape_type == 'auto':
+ shape_type = self._determineOptimalShape(model, bounds)
+
+ if shape_type == 'sphere':
+ return CollisionSphere(center, kwargs.get('radius', radius))
+
+ elif shape_type == 'box':
+ # 创建包围盒
+ min_point = bounds.getMin()
+ max_point = bounds.getMax()
+ return CollisionBox(min_point, max_point)
+
+ elif shape_type == 'capsule':
+ # 创建胶囊体(适合角色)
+ height = kwargs.get('height', (bounds.getMax().z - bounds.getMin().z))
+ radius = kwargs.get('radius', min(bounds.getRadius() * 0.5, height * 0.3))
+ point_a = Point3(center.x, center.y, bounds.getMin().z + radius)
+ point_b = Point3(center.x, center.y, bounds.getMax().z - radius)
+ return CollisionCapsule(point_a, point_b, radius)
+
+ elif shape_type == 'plane':
+ # 创建平面(适合地面、墙面)
+ normal = kwargs.get('normal', Vec3(0, 0, 1))
+ point = kwargs.get('point', center)
+ plane = Plane(normal, point)
+ return CollisionPlane(plane)
+
+ elif shape_type == 'polygon':
+ # === 创建多边形碰撞体 ===
+ # CollisionPolygon特点:
+ # - 单个平面多边形,所有顶点必须共面
+ # - 适用于:地板、墙面、简单平台等平面区域
+ # - 性能较好,但只能表示平面形状
+ #
+ # Mesh碰撞体特点:
+ # - 由多个三角形组成的复杂3D形状
+ # - 适用于:复杂地形、建筑物、不规则物体
+ # - 性能较差,但可以表示任意复杂形状
+
+ vertices = kwargs.get('vertices', [])
+ if len(vertices) >= 3:
+ # 将顶点转换为Point3对象
+ # 要求:所有顶点必须在同一平面上
+ collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices])
+ return collision_poly
+ else:
+ print("⚠️ 多边形至少需要3个顶点,回退到球体")
+ return CollisionSphere(center, radius)
+
+ def _determineOptimalShape(self, model, bounds):
+ """根据模型特征自动确定最适合的碰撞体形状"""
+ # 获取模型尺寸比例
+ size = bounds.getMax() - bounds.getMin()
+ max_dim = max(size.x, size.y, size.z)
+ min_dim = min(size.x, size.y, size.z)
+
+ # 根据模型名称和比例判断
+ model_name = model.getName().lower()
+
+ # 角色类模型使用胶囊体
+ if any(keyword in model_name for keyword in ['character', 'human', 'player', 'npc']):
+ return 'capsule'
+
+ # 建筑、箱子类使用包围盒
+ if any(keyword in model_name for keyword in ['building', 'box', 'cube', 'wall', 'door']):
+ return 'box'
+
+ # 地面、平台使用平面
+ if any(keyword in model_name for keyword in ['ground', 'floor', 'platform', 'terrain']):
+ return 'plane'
+
+ # 根据尺寸比例判断
+ aspect_ratio = max_dim / min_dim if min_dim > 0 else 1
+
+ if aspect_ratio > 3: # 细长物体用胶囊体
+ return 'capsule'
+ elif aspect_ratio < 1.5: # 接近球形用球体
+ return 'sphere'
+ else: # 其他用包围盒
+ return 'box'
+
+ def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']):
+ """创建鼠标射线"""
+ # 组合掩码
+ combined_mask = BitMask32.allOff()
+ for mask_type in mask_types:
+ if mask_type in self.MASKS:
+ combined_mask |= self.MASKS[mask_type]
+
+ # 坐标转换
+ near_point, far_point = self.world.event_handler.robustCoordinateConversion(
+ screen_x, screen_y)
+
+ if not near_point:
+ return None
+
+ # 创建射线节点
+ ray_node = CollisionNode('mouse_ray')
+ ray_node.setFromCollideMask(combined_mask)
+ ray_node.setIntoCollideMask(BitMask32.allOff())
+
+ # 创建射线
+ direction = far_point - near_point
+ direction.normalize()
+ ray = CollisionRay(near_point, direction)
+ ray_node.addSolid(ray)
+
+ return ray_node, near_point, far_point
+
+ def performRaycast(self, ray_node, scene_root=None):
+ """执行射线检测"""
+ if scene_root is None:
+ scene_root = self.world.render
+
+ # 创建临时节点路径
+ ray_np = self.world.cam.attachNewNode(ray_node)
+
+ try:
+ # 清除之前的结果
+ self.queue.clearEntries()
+
+ # 添加碰撞器
+ self.traverser.addCollider(ray_np, self.queue)
+
+ # 执行遍历
+ start_time = time.perf_counter()
+ self.traverser.traverse(scene_root)
+ detection_time = time.perf_counter() - start_time
+
+ # 记录性能
+ self.performance_monitor.recordDetection(
+ detection_time, self.queue.getNumEntries())
+
+ # 处理结果
+ results = []
+ if self.queue.getNumEntries() > 0:
+ self.queue.sortEntries()
+
+ for i in range(self.queue.getNumEntries()):
+ entry = self.queue.getEntry(i)
+ results.append({
+ 'hit_pos': entry.getSurfacePoint(scene_root),
+ 'hit_normal': entry.getSurfaceNormal(scene_root),
+ 'hit_node': entry.getIntoNodePath(),
+ 'distance': entry.getT()
+ })
+
+ return results
+
+ finally:
+ # 清理资源
+ self.traverser.removeCollider(ray_np)
+ ray_np.removeNode()
+
+ def getCollisionMask(self, mask_type):
+ """获取碰撞掩码"""
+ return self.MASKS.get(mask_type, BitMask32.allOff())
+
+ def createCollisionGroup(self, group_name, mask_types, enabled=True):
+ """创建碰撞分组
+
+ Args:
+ group_name: 分组名称
+ mask_types: 掩码类型列表
+ enabled: 是否启用
+ """
+ combined_mask = BitMask32.allOff()
+ for mask_type in mask_types:
+ if mask_type in self.MASKS:
+ combined_mask |= self.MASKS[mask_type]
+
+ self.collision_groups[group_name] = {
+ 'mask': combined_mask,
+ 'types': mask_types,
+ 'objects': []
+ }
+ self.group_enabled[group_name] = enabled
+
+ print(f"✅ 创建碰撞分组: {group_name}, 掩码: {mask_types}")
+
+ def addToCollisionGroup(self, group_name, model, collision_node):
+ """将模型添加到碰撞分组"""
+ if group_name in self.collision_groups:
+ self.collision_groups[group_name]['objects'].append({
+ 'model': model,
+ 'collision_node': collision_node
+ })
+
+ def enableCollisionGroup(self, group_name, enabled=True):
+ """启用/禁用碰撞分组"""
+ if group_name in self.group_enabled:
+ self.group_enabled[group_name] = enabled
+
+ # 更新分组中所有对象的碰撞状态
+ if group_name in self.collision_groups:
+ for obj in self.collision_groups[group_name]['objects']:
+ collision_node = obj['collision_node']
+ if enabled:
+ collision_node.show()
+ else:
+ collision_node.hide()
+
+ print(f"{'✅ 启用' if enabled else '❌ 禁用'}碰撞分组: {group_name}")
+
+ def registerCollisionCallback(self, mask_a, mask_b, callback_func, filter_func=None):
+ """注册碰撞回调函数
+
+ Args:
+ mask_a: 第一个掩码类型
+ mask_b: 第二个掩码类型
+ callback_func: 碰撞回调函数 callback(collision_info)
+ filter_func: 过滤函数 filter(model_a, model_b) -> bool
+ """
+ key = tuple(sorted([mask_a, mask_b]))
+ if key not in self.collision_callbacks:
+ self.collision_callbacks[key] = []
+
+ self.collision_callbacks[key].append(callback_func)
+
+ if filter_func:
+ if key not in self.collision_filters:
+ self.collision_filters[key] = []
+ self.collision_filters[key].append(filter_func)
+
+ print(f"✅ 注册碰撞回调: {mask_a} <-> {mask_b}")
+
+ def _executeCollisionCallbacks(self, collision_info):
+ """执行碰撞回调"""
+ model_a = collision_info['model_a']['node']
+ model_b = collision_info['model_b']['node']
+
+ # 获取模型的掩码类型
+ mask_a = self._getModelMaskType(model_a)
+ mask_b = self._getModelMaskType(model_b)
+
+ if mask_a and mask_b:
+ key = tuple(sorted([mask_a, mask_b]))
+
+ # 检查过滤条件
+ if key in self.collision_filters:
+ for filter_func in self.collision_filters[key]:
+ if not filter_func(model_a, model_b):
+ return
+
+ # 执行回调
+ if key in self.collision_callbacks:
+ for callback_func in self.collision_callbacks[key]:
+ try:
+ callback_func(collision_info)
+ except Exception as e:
+ print(f"❌ 碰撞回调执行失败: {e}")
+
+ def _getModelMaskType(self, model):
+ """获取模型的掩码类型"""
+ # 从模型标签或属性中获取掩码类型
+ if model.hasTag('collision_mask_type'):
+ return model.getTag('collision_mask_type')
+ return None
+
+ def enableSpatialPartitioning(self, enabled=True, max_depth=6, max_objects=10):
+ """启用空间分割优化
+
+ Args:
+ enabled: 是否启用
+ max_depth: 八叉树最大深度
+ max_objects: 每个节点最大对象数
+ """
+ self.spatial_partitioning_enabled = enabled
+ self.octree_max_depth = max_depth
+ self.octree_max_objects = max_objects
+
+ if enabled:
+ self._buildOctree()
+ print(f"✅ 启用空间分割优化 - 深度: {max_depth}, 对象数: {max_objects}")
+ else:
+ self.octree = None
+ print("❌ 禁用空间分割优化")
+
+ def _buildOctree(self):
+ """构建八叉树"""
+ if not hasattr(self.world, 'models') or not self.world.models:
+ return
+
+ # 计算场景边界
+ min_bound = Point3(float('inf'), float('inf'), float('inf'))
+ max_bound = Point3(float('-inf'), float('-inf'), float('-inf'))
+
+ for model in self.world.models:
+ bounds = model.getBounds()
+ if not bounds.isEmpty():
+ model_min = bounds.getMin()
+ model_max = bounds.getMax()
+
+ min_bound.x = min(min_bound.x, model_min.x)
+ min_bound.y = min(min_bound.y, model_min.y)
+ min_bound.z = min(min_bound.z, model_min.z)
+
+ max_bound.x = max(max_bound.x, model_max.x)
+ max_bound.y = max(max_bound.y, model_max.y)
+ max_bound.z = max(max_bound.z, model_max.z)
+
+ # 创建八叉树根节点
+ self.octree = OctreeNode(min_bound, max_bound, 0, self.octree_max_depth, self.octree_max_objects)
+
+ # 将模型插入八叉树
+ for model in self.world.models:
+ self.octree.insert(model)
+
+ def detectModelCollisionsOptimized(self, specific_models=None, log_results=True):
+ """使用空间分割优化的碰撞检测"""
+ if not self.spatial_partitioning_enabled or not self.octree:
+ return self.detectModelCollisions(specific_models, log_results)
+
+ start_time = time.perf_counter()
+ current_time = datetime.now()
+
+ models_to_check = specific_models if specific_models else self.world.models
+ if len(models_to_check) < 2:
+ return []
+
+ collision_results = []
+ current_collision_pairs = set()
+
+ # 使用八叉树进行优化检测
+ for model in models_to_check:
+ # 获取可能碰撞的模型列表
+ potential_collisions = self.octree.query(model)
+
+ for other_model in potential_collisions:
+ if model == other_model:
+ continue
+
+ pair_key = tuple(sorted([id(model), id(other_model)]))
+ if pair_key in current_collision_pairs:
+ continue
+
+ collision_info = self._checkModelPairCollision(model, other_model, current_time)
+ if collision_info:
+ collision_results.append(collision_info)
+ current_collision_pairs.add(pair_key)
+
+ # 执行回调
+ self._executeCollisionCallbacks(collision_info)
+
+ # 只有新碰撞才打印日志
+ if log_results and pair_key not in self.active_collisions:
+ self._logCollisionInfo(collision_info)
+ print(f"🆕 新碰撞检测到!")
+
+ # 更新活跃碰撞状态
+ ended_collisions = self.active_collisions - current_collision_pairs
+ for pair_key in ended_collisions:
+ print(f"✅ 碰撞结束: 模型对 {pair_key}")
+
+ self.active_collisions = current_collision_pairs
+
+ # 记录性能
+ detection_time = time.perf_counter() - start_time
+ self.performance_monitor.recordModelCollisionDetection(
+ detection_time, len(models_to_check), len(collision_results))
+
+ return collision_results
+
+ def cleanup(self):
+ """清理碰撞管理器资源"""
+ self._stopCollisionDetectionTask()
+ self.collision_history.clear()
+ self.active_collisions.clear()
+ print("✅ 碰撞管理器已清理")
+
+ def setupAdvancedCollision(self, model, shape_type='auto', mask_type='SELECTABLE',
+ group_name=None, **shape_kwargs):
+ """高级碰撞设置方法
+
+ Args:
+ model: 模型节点
+ shape_type: 碰撞体形状类型
+ mask_type: 掩码类型
+ group_name: 碰撞分组名称
+ **shape_kwargs: 形状特定参数
+
+ Returns:
+ collision_node_path: 碰撞节点路径
+ """
+ # 创建碰撞节点
+ cNode = CollisionNode(f'collision_{model.getName()}')
+
+ # 设置掩码
+ if mask_type in self.MASKS:
+ cNode.setIntoCollideMask(self.MASKS[mask_type])
+ # 设置模型标签用于回调识别
+ model.setTag('collision_mask_type', mask_type)
+
+ # 创建碰撞体
+ collision_solid = self.createCollisionShape(model, shape_type, **shape_kwargs)
+ cNode.addSolid(collision_solid)
+
+ # 附加到模型
+ cNodePath = model.attachNewNode(cNode)
+
+ # 添加到分组
+ if group_name:
+ if group_name not in self.collision_groups:
+ self.createCollisionGroup(group_name, [mask_type])
+ self.addToCollisionGroup(group_name, model, cNodePath)
+
+ # 根据调试设置决定是否显示
+ if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
+ cNodePath.show()
+ else:
+ cNodePath.hide()
+
+ print(f"✅ 为 {model.getName()} 设置高级碰撞 - 形状: {shape_type}, 掩码: {mask_type}")
+ return cNodePath
+
+ def example_usage(self):
+ """碰撞系统使用示例"""
+ print("\n=== 碰撞系统使用示例 ===")
+
+ # 1. 创建碰撞分组
+ self.createCollisionGroup('characters', ['CHARACTER', 'DYNAMIC_OBJECT'])
+ self.createCollisionGroup('environment', ['STATIC_GEOMETRY', 'TERRAIN'])
+ self.createCollisionGroup('pickups', ['PICKUP_ITEM', 'INTERACTIVE'])
+
+ # 2. 注册碰撞回调
+ def character_pickup_callback(collision_info):
+ print(f"角色拾取物品: {collision_info['model_a']['name']} -> {collision_info['model_b']['name']}")
+
+ def character_environment_callback(collision_info):
+ print(f"角色碰撞环境: {collision_info['model_a']['name']} 碰到 {collision_info['model_b']['name']}")
+
+ self.registerCollisionCallback('CHARACTER', 'PICKUP_ITEM', character_pickup_callback)
+ self.registerCollisionCallback('CHARACTER', 'STATIC_GEOMETRY', character_environment_callback)
+
+ # 3. 启用空间分割优化
+ self.enableSpatialPartitioning(enabled=True, max_depth=6, max_objects=8)
+
+ # 4. 为模型设置不同类型的碰撞体
+ # model1 = ... # 假设已有模型
+ # self.setupAdvancedCollision(model1, 'capsule', 'CHARACTER', 'characters')
+ # self.setupAdvancedCollision(model2, 'box', 'PICKUP_ITEM', 'pickups')
+ # self.setupAdvancedCollision(model3, 'plane', 'TERRAIN', 'environment')
+
+ print("✅ 碰撞系统示例配置完成")
+
+class CollisionPerformanceMonitor:
+ """碰撞检测性能监控器"""
+
+ def __init__(self):
+ self.detection_times = []
+ self.collision_counts = []
+ self.model_collision_times = [] # 新增:模型间碰撞检测时间
+ self.model_collision_data = [] # 新增:模型间碰撞数据
+ self.max_records = 100
+
+ def recordDetection(self, detection_time, collision_count):
+ """记录射线检测性能"""
+ self.detection_times.append(detection_time)
+ self.collision_counts.append(collision_count)
+
+ # 限制记录数量
+ if len(self.detection_times) > self.max_records:
+ self.detection_times.pop(0)
+ self.collision_counts.pop(0)
+
+ def recordModelCollisionDetection(self, detection_time, model_count, collision_count):
+ """记录模型间碰撞检测性能"""
+ self.model_collision_times.append(detection_time)
+ self.model_collision_data.append({
+ 'model_count': model_count,
+ 'collision_count': collision_count,
+ 'timestamp': time.time()
+ })
+
+ # 限制记录数量
+ if len(self.model_collision_times) > self.max_records:
+ self.model_collision_times.pop(0)
+ self.model_collision_data.pop(0)
+
+ def getAverageTime(self):
+ """获取平均射线检测时间"""
+ if not self.detection_times:
+ return 0
+ return sum(self.detection_times) / len(self.detection_times)
+
+ def getAverageModelCollisionTime(self):
+ """获取平均模型间碰撞检测时间"""
+ if not self.model_collision_times:
+ return 0
+ return sum(self.model_collision_times) / len(self.model_collision_times)
+
+ def getPerformanceReport(self):
+ """获取完整性能报告"""
+ report = []
+
+ # 射线检测性能
+ if self.detection_times:
+ avg_time = self.getAverageTime()
+ max_time = max(self.detection_times)
+ avg_collisions = sum(self.collision_counts) / len(self.collision_counts)
+
+ report.append("=== 射线检测性能 ===")
+ report.append(f"平均检测时间: {avg_time*1000:.2f}ms")
+ report.append(f"最大检测时间: {max_time*1000:.2f}ms")
+ report.append(f"平均碰撞数量: {avg_collisions:.1f}")
+ report.append(f"检测次数: {len(self.detection_times)}")
+
+ # 模型间碰撞检测性能
+ if self.model_collision_times:
+ avg_model_time = self.getAverageModelCollisionTime()
+ max_model_time = max(self.model_collision_times)
+ avg_model_count = sum(d['model_count'] for d in self.model_collision_data) / len(self.model_collision_data)
+ total_collisions = sum(d['collision_count'] for d in self.model_collision_data)
+
+ report.append("\n=== 模型间碰撞检测性能 ===")
+ report.append(f"平均检测时间: {avg_model_time*1000:.2f}ms")
+ report.append(f"最大检测时间: {max_model_time*1000:.2f}ms")
+ report.append(f"平均模型数量: {avg_model_count:.1f}")
+ report.append(f"总碰撞次数: {total_collisions}")
+ report.append(f"检测次数: {len(self.model_collision_times)}")
+
+ return "\n".join(report) if report else "没有性能数据"
+
+class OctreeNode:
+ """八叉树节点"""
+
+ def __init__(self, min_bound, max_bound, depth, max_depth, max_objects):
+ self.min_bound = min_bound
+ self.max_bound = max_bound
+ self.depth = depth
+ self.max_depth = max_depth
+ self.max_objects = max_objects
+
+ self.objects = []
+ self.children = []
+ self.is_leaf = True
+
+ def insert(self, model):
+ """插入模型到八叉树"""
+ if not self._contains(model):
+ return False
+
+ if self.is_leaf:
+ self.objects.append(model)
+
+ # 如果超过最大对象数且未达到最大深度,则分割
+ if len(self.objects) > self.max_objects and self.depth < self.max_depth:
+ self._subdivide()
+
+ # 重新分配对象到子节点
+ remaining_objects = []
+ for obj in self.objects:
+ inserted = False
+ for child in self.children:
+ if child.insert(obj):
+ inserted = True
+ break
+ if not inserted:
+ remaining_objects.append(obj)
+
+ self.objects = remaining_objects
+ else:
+ # 尝试插入到子节点
+ inserted = False
+ for child in self.children:
+ if child.insert(model):
+ inserted = True
+ break
+
+ if not inserted:
+ self.objects.append(model)
+
+ return True
+
+ def query(self, model):
+ """查询可能与指定模型碰撞的模型列表"""
+ if not self._contains(model):
+ return []
+
+ result = list(self.objects)
+
+ if not self.is_leaf:
+ for child in self.children:
+ result.extend(child.query(model))
+
+ return result
+
+ def _contains(self, model):
+ """检查模型是否在此节点范围内"""
+ bounds = model.getBounds()
+ if bounds.isEmpty():
+ return False
+
+ model_min = bounds.getMin()
+ model_max = bounds.getMax()
+
+ return (model_max.x >= self.min_bound.x and model_min.x <= self.max_bound.x and
+ model_max.y >= self.min_bound.y and model_min.y <= self.max_bound.y and
+ model_max.z >= self.min_bound.z and model_min.z <= self.max_bound.z)
+
+ def _subdivide(self):
+ """分割节点为8个子节点"""
+ center = Point3(
+ (self.min_bound.x + self.max_bound.x) * 0.5,
+ (self.min_bound.y + self.max_bound.y) * 0.5,
+ (self.min_bound.z + self.max_bound.z) * 0.5
+ )
+
+ # 创建8个子节点
+ subdivisions = [
+ (self.min_bound, center),
+ (Point3(center.x, self.min_bound.y, self.min_bound.z), Point3(self.max_bound.x, center.y, center.z)),
+ (Point3(self.min_bound.x, center.y, self.min_bound.z), Point3(center.x, self.max_bound.y, center.z)),
+ (Point3(center.x, center.y, self.min_bound.z), Point3(self.max_bound.x, self.max_bound.y, center.z)),
+ (Point3(self.min_bound.x, self.min_bound.y, center.z), Point3(center.x, center.y, self.max_bound.z)),
+ (Point3(center.x, self.min_bound.y, center.z), Point3(self.max_bound.x, center.y, self.max_bound.z)),
+ (Point3(self.min_bound.x, center.y, center.z), Point3(center.x, self.max_bound.y, self.max_bound.z)),
+ (center, self.max_bound)
+ ]
+
+ for min_b, max_b in subdivisions:
+ child = OctreeNode(min_b, max_b, self.depth + 1, self.max_depth, self.max_objects)
+ self.children.append(child)
+
+ self.is_leaf = False
diff --git a/gui/gui_manager.py b/gui/gui_manager.py
index 550f0bb3..2029888e 100644
--- a/gui/gui_manager.py
+++ b/gui/gui_manager.py
@@ -41,180 +41,568 @@ class GUIManager:
self.currentGUITool = None
print("✓ GUI管理系统初始化完成")
-
+
# ==================== GUI元素创建方法 ====================
-
+
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
- """创建2D GUI按钮"""
- from direct.gui.DirectGui import DirectButton
-
- # 将3D坐标转换为2D屏幕坐标
- gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
-
- button = DirectButton(
- text=text,
- pos=gui_pos,
- scale=size,
- command=self.onGUIButtonClick,
- extraArgs=[f"button_{len(self.gui_elements)}"],
- frameColor=(0.2, 0.6, 0.8, 1),
- text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
- rolloverSound=None,
- clickSound=None
- )
-
- # 为GUI元素添加标识
- button.setTag("gui_type", "button")
- button.setTag("gui_id", f"button_{len(self.gui_elements)}")
- button.setTag("gui_text", text)
- button.setTag("is_gui_element", "1")
-
- self.gui_elements.append(button)
- # 安全地调用updateSceneTree
- if hasattr(self.world, 'updateSceneTree'):
- self.world.updateSceneTree()
-
- print(f"✓ 创建GUI按钮: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
- return button
-
+ """创建2D GUI按钮 - 支持多选创建和GUI父子关系,优化版本"""
+ try:
+ from direct.gui.DirectGui import DirectButton
+ from PyQt5.QtCore import Qt
+
+ print(f"🔘 开始创建GUI按钮,位置: {pos}, 文本: {text}, 尺寸: {size}")
+
+ # 获取树形控件
+ tree_widget = self._get_tree_widget()
+ if not tree_widget:
+ print("❌ 无法访问树形控件")
+ return None
+
+ # 使用CustomTreeWidget的方法获取目标父节点列表
+ target_parents = tree_widget.get_target_parents_for_gui_creation()
+ if not target_parents:
+ print("❌ 没有找到有效的父节点")
+ return None
+
+ created_buttons = []
+
+ # 为每个有效的父节点创建GUI按钮
+ for parent_item, parent_node in target_parents:
+ try:
+ # 生成唯一名称
+ button_name = f"GUIButton_{len(self.gui_elements)}"
+
+ # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式
+ if tree_widget.is_gui_element(parent_node):
+ # 父节点是GUI元素 - 作为子GUI挂载
+ gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
+ parent_gui_node = parent_node # 直接挂载到GUI元素
+ print(f"📎 挂载到GUI父节点: {parent_node.getName()}")
+ else:
+ # 父节点是普通3D节点 - 使用屏幕坐标
+ gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
+ parent_gui_node = None # 使用默认的aspect2d
+ print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
+
+ button = DirectButton(
+ text=text,
+ pos=gui_pos,
+ scale=size,
+ command=self.onGUIButtonClick,
+ extraArgs=[f"button_{len(self.gui_elements)}"],
+ frameColor=(0.2, 0.6, 0.8, 1),
+ text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
+ rolloverSound=None,
+ clickSound=None,
+ parent=parent_gui_node # 设置GUI父节点
+ )
+
+ # 设置节点标签
+ button.setTag("gui_type", "button")
+ button.setTag("gui_id", f"button_{len(self.gui_elements)}")
+ button.setTag("gui_text", text)
+ button.setTag("is_gui_element", "1")
+ button.setTag("is_scene_element", "1")
+ button.setTag("created_by_user", "1")
+ button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
+ button.setName(button_name)
+
+ # 如果有GUI父节点,建立引用关系
+ if parent_gui_node:
+ parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else ""
+ button.setTag("gui_parent_id", parent_id)
+
+ # 添加到GUI元素列表
+ self.gui_elements.append(button)
+
+ print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {button_name}")
+
+ # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
+ qt_item = tree_widget.add_node_to_tree_widget(button, parent_item, "GUI_BUTTON")
+ if qt_item:
+ created_buttons.append((button, qt_item))
+ else:
+ created_buttons.append((button, None))
+ print("⚠️ Qt树节点添加失败,但GUI对象已创建")
+
+ except Exception as e:
+ print(f"❌ 为 {parent_item.text(0)} 创建GUI按钮失败: {str(e)}")
+ continue
+
+ # 处理创建结果
+ if not created_buttons:
+ print("❌ 没有成功创建任何GUI按钮")
+ return None
+
+ # 选中最后创建的按钮并更新场景树
+ if created_buttons:
+ last_button, last_qt_item = created_buttons[-1]
+ if last_qt_item:
+ tree_widget.setCurrentItem(last_qt_item)
+ tree_widget.update_selection_and_properties(last_button, last_qt_item)
+
+ print(f"🎉 总共创建了 {len(created_buttons)} 个GUI按钮")
+
+ # 返回值处理
+ if len(created_buttons) == 1:
+ return created_buttons[0][0]
+ else:
+ return [button for button, _ in created_buttons]
+
+ except Exception as e:
+ print(f"❌ 创建GUI按钮过程失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
+
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
- """创建2D GUI标签"""
- from direct.gui.DirectGui import DirectLabel
-
- # 将3D坐标转换为2D屏幕坐标
- gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
-
- label = DirectLabel(
- text=text,
- pos=gui_pos,
- scale=size,
- frameColor=(0, 0, 0, 0), # 透明背景
- text_fg=(1, 1, 1, 1),
- text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
- )
-
- # 为GUI元素添加标识
- label.setTag("gui_type", "label")
- label.setTag("gui_id", f"label_{len(self.gui_elements)}")
- label.setTag("gui_text", text)
- label.setTag("is_gui_element", "1")
-
- self.gui_elements.append(label)
- # 安全地调用updateSceneTree
- if hasattr(self.world, 'updateSceneTree'):
- self.world.updateSceneTree()
-
- print(f"✓ 创建GUI标签: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
- return label
-
+ """创建2D GUI标签 - 支持多选创建和GUI父子关系,优化版本"""
+ try:
+ from direct.gui.DirectGui import DirectLabel
+ from PyQt5.QtCore import Qt
+
+ print(f"🏷️ 开始创建GUI标签,位置: {pos}, 文本: {text}, 尺寸: {size}")
+
+ # 获取树形控件
+ tree_widget = self._get_tree_widget()
+ if not tree_widget:
+ print("❌ 无法访问树形控件")
+ return None
+
+ # 使用CustomTreeWidget的方法获取目标父节点列表
+ target_parents = tree_widget.get_target_parents_for_gui_creation()
+ if not target_parents:
+ print("❌ 没有找到有效的父节点")
+ return None
+
+ created_labels = []
+
+ # 为每个有效的父节点创建GUI标签
+ for parent_item, parent_node in target_parents:
+ try:
+ # 生成唯一名称
+ label_name = f"GUILabel_{len(self.gui_elements)}"
+
+ # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式
+ if tree_widget.is_gui_element(parent_node):
+ # 父节点是GUI元素 - 作为子GUI挂载
+ gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
+ parent_gui_node = parent_node
+ print(f"📎 挂载到GUI父节点: {parent_node.getName()}")
+ else:
+ # 父节点是普通3D节点 - 使用屏幕坐标
+ gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
+ parent_gui_node = None
+ print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
+
+ label = DirectLabel(
+ text=text,
+ pos=gui_pos,
+ scale=size,
+ frameColor=(0, 0, 0, 0), # 透明背景
+ text_fg=(1, 1, 1, 1),
+ text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
+ parent=parent_gui_node # 设置GUI父节点
+ )
+
+ # 设置节点标签
+ label.setTag("gui_type", "label")
+ label.setTag("gui_id", f"label_{len(self.gui_elements)}")
+ label.setTag("gui_text", text)
+ label.setTag("is_gui_element", "1")
+ label.setTag("is_scene_element", "1")
+ label.setTag("created_by_user", "1")
+ label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
+ label.setName(label_name)
+
+ # 如果有GUI父节点,建立引用关系
+ if parent_gui_node:
+ parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else ""
+ label.setTag("gui_parent_id", parent_id)
+
+ # 添加到GUI元素列表
+ self.gui_elements.append(label)
+
+ print(f"✅ 为 {parent_item.text(0)} 创建GUI标签成功: {label_name}")
+
+ # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
+ qt_item = tree_widget.add_node_to_tree_widget(label, parent_item, "GUI_LABEL")
+ if qt_item:
+ created_labels.append((label, qt_item))
+ else:
+ created_labels.append((label, None))
+ print("⚠️ Qt树节点添加失败,但GUI对象已创建")
+
+ except Exception as e:
+ print(f"❌ 为 {parent_item.text(0)} 创建GUI标签失败: {str(e)}")
+ continue
+
+ # 处理创建结果
+ if not created_labels:
+ print("❌ 没有成功创建任何GUI标签")
+ return None
+
+ # 选中最后创建的标签并更新场景树
+ if created_labels:
+ last_label, last_qt_item = created_labels[-1]
+ if last_qt_item:
+ tree_widget.setCurrentItem(last_qt_item)
+ tree_widget.update_selection_and_properties(last_label, last_qt_item)
+
+ print(f"🎉 总共创建了 {len(created_labels)} 个GUI标签")
+
+ # 返回值处理
+ if len(created_labels) == 1:
+ return created_labels[0][0]
+ else:
+ return [label for label, _ in created_labels]
+
+ except Exception as e:
+ print(f"❌ 创建GUI标签过程失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
+
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
- """创建2D GUI文本输入框"""
- from direct.gui.DirectGui import DirectEntry
-
- # 将3D坐标转换为2D屏幕坐标
- gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
-
- entry = DirectEntry(
- text="",
- pos=gui_pos,
- scale=size,
- command=self.onGUIEntrySubmit,
- extraArgs=[f"entry_{len(self.gui_elements)}"],
- initialText=placeholder,
- numLines=1,
- width=12,
- focus=0
- )
-
- # 为GUI元素添加标识
- entry.setTag("gui_type", "entry")
- entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
- entry.setTag("gui_placeholder", placeholder)
- entry.setTag("is_gui_element", "1")
-
- self.gui_elements.append(entry)
- # 安全地调用updateSceneTree
- if hasattr(self.world, 'updateSceneTree'):
- self.world.updateSceneTree()
-
- print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
- return entry
-
+ """创建2D GUI文本输入框 - 支持多选创建和GUI父子关系,优化版本"""
+ try:
+ from direct.gui.DirectGui import DirectEntry
+ from PyQt5.QtCore import Qt
+
+ print(f"📝 开始创建GUI输入框,位置: {pos}, 占位符: {placeholder}, 尺寸: {size}")
+
+ # 获取树形控件
+ tree_widget = self._get_tree_widget()
+ if not tree_widget:
+ print("❌ 无法访问树形控件")
+ return None
+
+ # 使用CustomTreeWidget的方法获取目标父节点列表
+ target_parents = tree_widget.get_target_parents_for_gui_creation()
+ if not target_parents:
+ print("❌ 没有找到有效的父节点")
+ return None
+
+ created_entries = []
+
+ # 为每个有效的父节点创建GUI输入框
+ for parent_item, parent_node in target_parents:
+ try:
+ # 生成唯一名称
+ entry_name = f"GUIEntry_{len(self.gui_elements)}"
+
+ # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式
+ if tree_widget.is_gui_element(parent_node):
+ # 父节点是GUI元素 - 作为子GUI挂载
+ gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
+ parent_gui_node = parent_node
+ print(f"📎 挂载到GUI父节点: {parent_node.getName()}")
+ else:
+ # 父节点是普通3D节点 - 使用屏幕坐标
+ gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
+ parent_gui_node = None
+ print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
+
+ entry = DirectEntry(
+ text="",
+ pos=gui_pos,
+ scale=size,
+ command=self.onGUIEntrySubmit,
+ extraArgs=[f"entry_{len(self.gui_elements)}"],
+ initialText=placeholder,
+ numLines=1,
+ width=12,
+ focus=0,
+ parent=parent_gui_node # 设置GUI父节点
+ )
+
+ # 设置节点标签
+ entry.setTag("gui_type", "entry")
+ entry.setTag("gui_id", f"entry_{len(self.gui_elements)}")
+ entry.setTag("gui_placeholder", placeholder)
+ entry.setTag("is_gui_element", "1")
+ entry.setTag("is_scene_element", "1")
+ entry.setTag("created_by_user", "1")
+ entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d")
+ entry.setName(entry_name)
+
+ # 如果有GUI父节点,建立引用关系
+ if parent_gui_node:
+ parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else ""
+ entry.setTag("gui_parent_id", parent_id)
+
+ # 添加到GUI元素列表
+ self.gui_elements.append(entry)
+
+ print(f"✅ 为 {parent_item.text(0)} 创建GUI输入框成功: {entry_name}")
+
+ # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
+ qt_item = tree_widget.add_node_to_tree_widget(entry, parent_item, "GUI_ENTRY")
+ if qt_item:
+ created_entries.append((entry, qt_item))
+ else:
+ created_entries.append((entry, None))
+ print("⚠️ Qt树节点添加失败,但GUI对象已创建")
+
+ except Exception as e:
+ print(f"❌ 为 {parent_item.text(0)} 创建GUI输入框失败: {str(e)}")
+ continue
+
+ # 处理创建结果
+ if not created_entries:
+ print("❌ 没有成功创建任何GUI输入框")
+ return None
+
+ # 选中最后创建的输入框并更新场景树
+ if created_entries:
+ last_entry, last_qt_item = created_entries[-1]
+ if last_qt_item:
+ tree_widget.setCurrentItem(last_qt_item)
+ tree_widget.update_selection_and_properties(last_entry, last_qt_item)
+
+ print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框")
+
+ # 返回值处理
+ if len(created_entries) == 1:
+ return created_entries[0][0]
+ else:
+ return [entry for entry, _ in created_entries]
+
+ except Exception as e:
+ print(f"❌ 创建GUI输入框过程失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
+
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
- """创建3D空间文本"""
- from panda3d.core import TextNode
-
- textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
- textNode.setText(text)
- textNode.setAlign(TextNode.ACenter)
- if self.world.getChineseFont():
- textNode.setFont(self.world.getChineseFont())
-
- textNodePath = self.world.render.attachNewNode(textNode)
- textNodePath.setPos(*pos)
- textNodePath.setScale(size)
- textNodePath.setColor(1, 1, 0, 1)
- textNodePath.setBillboardAxis() # 让文本总是面向相机
-
- # 为GUI元素添加标识
- textNodePath.setTag("gui_type", "3d_text")
- textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}")
- textNodePath.setTag("gui_text", text)
- textNodePath.setTag("is_gui_element", "1")
-
- self.gui_elements.append(textNodePath)
- # 安全地调用updateSceneTree
- if hasattr(self.world, 'updateSceneTree'):
- self.world.updateSceneTree()
-
- print(f"✓ 创建3D文本: {text} (世界位置: {pos})")
- return textNodePath
+ """创建3D空间文本 - 支持多选创建,优化版本"""
+ try:
+ from panda3d.core import TextNode
+ from PyQt5.QtCore import Qt
+ print(f"📄 开始创建3D文本,位置: {pos}, 文本: {text}, 尺寸: {size}")
+
+ # 获取树形控件
+ tree_widget = self._get_tree_widget()
+ if not tree_widget:
+ print("❌ 无法访问树形控件")
+ return None
+
+ # 获取目标父节点列表
+ target_parents = tree_widget.get_target_parents_for_creation()
+ if not target_parents:
+ print("❌ 没有找到有效的父节点")
+ return None
+
+ created_texts = []
+
+ # 为每个有效的父节点创建3D文本
+ for parent_item, parent_node in target_parents:
+ try:
+ # 生成唯一名称
+ text_name = f"GUI3DText_{len(self.gui_elements)}"
+
+ textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
+ textNode.setText(text)
+ textNode.setAlign(TextNode.ACenter)
+ if self.world.getChineseFont():
+ textNode.setFont(self.world.getChineseFont())
+
+ # 挂载到选中的父节点
+ textNodePath = parent_node.attachNewNode(textNode)
+ textNodePath.setPos(*pos)
+ textNodePath.setScale(size)
+ textNodePath.setColor(1, 1, 0, 1)
+ textNodePath.setBillboardAxis() # 让文本总是面向相机
+ textNodePath.setName(text_name)
+
+ # 设置节点标签
+ textNodePath.setTag("gui_type", "3d_text")
+ textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}")
+ textNodePath.setTag("gui_text", text)
+ textNodePath.setTag("is_gui_element", "1")
+ textNodePath.setTag("is_scene_element", "1")
+ textNodePath.setTag("created_by_user", "1")
+
+ # 添加到GUI元素列表
+ self.gui_elements.append(textNodePath)
+
+ print(f"✅ 为 {parent_item.text(0)} 创建3D文本成功: {text_name}")
+
+ # 在Qt树形控件中添加对应节点
+ qt_item = tree_widget.add_node_to_tree_widget(textNodePath, parent_item, "GUI_3DTEXT")
+ if qt_item:
+ created_texts.append((textNodePath, qt_item))
+ else:
+ created_texts.append((textNodePath, None))
+ print("⚠️ Qt树节点添加失败,但GUI对象已创建")
+
+ except Exception as e:
+ print(f"❌ 为 {parent_item.text(0)} 创建3D文本失败: {str(e)}")
+ continue
+
+ # 处理创建结果
+ if not created_texts:
+ print("❌ 没有成功创建任何3D文本")
+ return None
+
+ # 选中最后创建的文本并更新场景树
+ if created_texts:
+ last_text, last_qt_item = created_texts[-1]
+ if last_qt_item:
+ tree_widget.setCurrentItem(last_qt_item)
+ tree_widget.update_selection_and_properties(last_text, last_qt_item)
+
+ print(f"🎉 总共创建了 {len(created_texts)} 个3D文本")
+
+ # 返回值处理
+ if len(created_texts) == 1:
+ return created_texts[0][0]
+ else:
+ return [text_np for text_np, _ in created_texts]
+
+ except Exception as e:
+ print(f"❌ 创建3D文本过程失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
-
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
- """创建3D虚拟屏幕"""
- from panda3d.core import CardMaker, TransparencyAttrib, TextNode
-
- # 创建虚拟屏幕
- cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}")
- cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2)
- virtualScreen = self.world.render.attachNewNode(cm.generate())
- virtualScreen.setPos(*pos)
- virtualScreen.setColor(0.2, 0.2, 0.2, 0.8)
- virtualScreen.setTransparency(TransparencyAttrib.MAlpha)
-
- # 在虚拟屏幕上添加文本
- screenText = TextNode(f'screen-text-{len(self.gui_elements)}')
- screenText.setText(text)
- screenText.setAlign(TextNode.ACenter)
- if self.world.getChineseFont():
- screenText.setFont(self.world.getChineseFont())
- screenTextNP = virtualScreen.attachNewNode(screenText)
- screenTextNP.setPos(0, 0.01, 0)
- screenTextNP.setScale(0.3)
- screenTextNP.setColor(0, 1, 0, 1)
-
- # 为GUI元素添加标识
- virtualScreen.setTag("gui_type", "virtual_screen")
- virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}")
- virtualScreen.setTag("gui_text", text)
- virtualScreen.setTag("is_gui_element", "1")
-
- self.gui_elements.append(virtualScreen)
- # 安全地调用updateSceneTree
- if hasattr(self.world, 'updateSceneTree'):
- self.world.updateSceneTree()
-
- print(f"✓ 创建虚拟屏幕: {text} (世界位置: {pos})")
- return virtualScreen
-
+ """创建3D虚拟屏幕 - 支持多选创建,优化版本"""
+ try:
+ from panda3d.core import CardMaker, TransparencyAttrib, TextNode
+ from PyQt5.QtCore import Qt
+
+ print(f"🖥️ 开始创建虚拟屏幕,位置: {pos}, 尺寸: {size}, 文本: {text}")
+
+ # 获取树形控件
+ tree_widget = self._get_tree_widget()
+ if not tree_widget:
+ print("❌ 无法访问树形控件")
+ return None
+
+ # 获取目标父节点列表
+ target_parents = tree_widget.get_target_parents_for_creation()
+ if not target_parents:
+ print("❌ 没有找到有效的父节点")
+ return None
+
+ created_screens = []
+
+ # 为每个有效的父节点创建虚拟屏幕
+ for parent_item, parent_node in target_parents:
+ try:
+ # 生成唯一名称
+ screen_name = f"VirtualScreen_{len(self.gui_elements)}"
+
+ # 创建虚拟屏幕几何体
+ cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}")
+ cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2)
+
+ # 创建挂载节点 - 挂载到选中的父节点
+ virtual_screen = parent_node.attachNewNode(cm.generate())
+ virtual_screen.setPos(*pos)
+ virtual_screen.setName(screen_name)
+ virtual_screen.setColor(0.2, 0.2, 0.2, 0.8)
+ virtual_screen.setTransparency(TransparencyAttrib.MAlpha)
+
+ # 在虚拟屏幕上添加文本
+ screen_text_node = self._create_screen_text(virtual_screen, text, len(self.gui_elements))
+
+ # 设置节点标签
+ virtual_screen.setTag("gui_type", "virtual_screen")
+ virtual_screen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}")
+ virtual_screen.setTag("gui_text", text)
+ virtual_screen.setTag("is_gui_element", "1")
+ virtual_screen.setTag("is_scene_element", "1")
+ virtual_screen.setTag("created_by_user", "1")
+
+ # 添加到GUI元素列表
+ self.gui_elements.append(virtual_screen)
+
+ print(f"✅ 为 {parent_item.text(0)} 创建虚拟屏幕成功: {screen_name}")
+
+ # 在Qt树形控件中添加对应节点
+ qt_item = tree_widget.add_node_to_tree_widget(virtual_screen, parent_item, "GUI_VirtualScreen")
+ if qt_item:
+ created_screens.append((virtual_screen, qt_item))
+ else:
+ created_screens.append((virtual_screen, None))
+ print("⚠️ Qt树节点添加失败,但Panda3D对象已创建")
+
+ except Exception as e:
+ print(f"❌ 为 {parent_item.text(0)} 创建虚拟屏幕失败: {str(e)}")
+ continue
+
+ # 处理创建结果
+ if not created_screens:
+ print("❌ 没有成功创建任何虚拟屏幕")
+ return None
+
+ # 选中最后创建的虚拟屏幕
+ if created_screens:
+ last_screen_np, last_qt_item = created_screens[-1]
+ if last_qt_item:
+ tree_widget.setCurrentItem(last_qt_item)
+ # 更新选择和属性面板
+ tree_widget.update_selection_and_properties(last_screen_np, last_qt_item)
+
+ print(f"🎉 总共创建了 {len(created_screens)} 个虚拟屏幕")
+
+ # 返回值处理
+ if len(created_screens) == 1:
+ return created_screens[0][0] # 单个屏幕返回NodePath
+ else:
+ return [screen_np for screen_np, _ in created_screens] # 多个屏幕返回列表
+
+ except Exception as e:
+ print(f"❌ 创建虚拟屏幕过程失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
+
+ def _create_screen_text(self, virtual_screen, text, screen_index):
+ """为虚拟屏幕创建文本节点"""
+ try:
+ from panda3d.core import TextNode
+
+ screen_text = TextNode(f'screen-text-{screen_index}')
+ screen_text.setText(text)
+ screen_text.setAlign(TextNode.ACenter)
+
+ # 设置中文字体
+ if hasattr(self.world, 'getChineseFont') and self.world.getChineseFont():
+ screen_text.setFont(self.world.getChineseFont())
+
+ # 创建文本节点路径并设置属性
+ screen_text_np = virtual_screen.attachNewNode(screen_text)
+ screen_text_np.setPos(0, 0.01, 0) # 稍微向前偏移避免Z-fighting
+ screen_text_np.setScale(0.3)
+ screen_text_np.setColor(0, 1, 0, 1) # 绿色文本
+
+ print(f"✅ 虚拟屏幕文本创建成功: {text}")
+ return screen_text_np
+
+ except Exception as e:
+ print(f"❌ 创建虚拟屏幕文本失败: {str(e)}")
+ return None
+
+ def _get_tree_widget(self):
+ """安全获取树形控件"""
+ try:
+ if (hasattr(self.world, 'interface_manager') and
+ hasattr(self.world.interface_manager, 'treeWidget')):
+ return self.world.interface_manager.treeWidget
+ except AttributeError:
+ pass
+ return None
+
def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
"""创建2D GUI滑块"""
from direct.gui.DirectGui import DirectSlider
-
+
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
-
+
slider = DirectSlider(
pos=gui_pos,
scale=scale,
@@ -223,20 +611,21 @@ class GUIManager:
frameColor=(0.6, 0.6, 0.6, 1),
thumbColor=(0.2, 0.8, 0.2, 1)
)
-
+
slider.setTag("gui_type", "slider")
slider.setTag("gui_id", f"slider_{len(self.gui_elements)}")
slider.setTag("gui_text", text)
slider.setTag("is_gui_element", "1")
-
+
self.gui_elements.append(slider)
# 安全地调用updateSceneTree
if hasattr(self.world, 'updateSceneTree'):
+ pass # CH
self.world.updateSceneTree()
-
+
print(f"✓ 创建GUI滑块: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
return slider
-
+
# ==================== GUI元素管理方法 ====================
def deleteGUIElement(self, gui_element):
@@ -937,7 +1326,7 @@ class GUIManager:
"""编辑2D GUI元素位置"""
try:
current_pos = gui_element.getPos()
-
+
if axis == "x":
# 将逻辑坐标转换为屏幕坐标
new_screen_x = value * 0.1
@@ -946,8 +1335,8 @@ class GUIManager:
# 将逻辑坐标转换为屏幕坐标
new_screen_z = value * 0.1
gui_element.setPos(current_pos.getX(), current_pos.getY(), new_screen_z)
-
+
print(f"更新2D GUI位置: {axis}轴 = {value} (屏幕坐标: {gui_element.getPos()})")
-
+
except Exception as e:
print(f"编辑2D GUI位置失败: {str(e)}")
\ No newline at end of file
diff --git a/icons/move_tool.png b/icons/move_tool.png
new file mode 100644
index 00000000..37aa5d0e
Binary files /dev/null and b/icons/move_tool.png differ
diff --git a/icons/rotate_tool.png b/icons/rotate_tool.png
new file mode 100644
index 00000000..1cb82e61
Binary files /dev/null and b/icons/rotate_tool.png differ
diff --git a/icons/scale_tool.png b/icons/scale_tool.png
new file mode 100644
index 00000000..9d95774e
Binary files /dev/null and b/icons/scale_tool.png differ
diff --git a/icons/select_tool.png b/icons/select_tool.png
new file mode 100644
index 00000000..cfbaa7d4
Binary files /dev/null and b/icons/select_tool.png differ
diff --git a/main.py b/main.py
index 065ef800..3ae099e4 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,7 @@
import warnings
+
+from demo.video_integration import VideoManager
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
import sys
@@ -62,6 +65,9 @@ class MyWorld(CoreWorld):
# 初始化GUI管理系统
self.gui_manager = GUIManager(self)
+
+ # 初始化视频管理
+ self.video_manager = VideoManager(self)
# 初始化场景管理系统
self.scene_manager = SceneManager(self)
@@ -85,27 +91,38 @@ class MyWorld(CoreWorld):
#self.material_editor = None
+ # 初始化碰撞管理器
+ from core.collision_manager import CollisionManager
+ self.collision_manager = CollisionManager(self)
+
+ # 调试选项
+ self.debug_collision = True # 是否显示碰撞体
+
+ # 默认启用模型间碰撞检测(可选)
+ self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5)
+
print("✓ MyWorld 初始化完成")
-
+ print("✅ 碰撞管理器已初始化")
+
# ==================== 兼容性属性 ====================
-
+
# 保留models属性以兼容现有代码
@property
def models(self):
"""模型列表的兼容性属性"""
return self.scene_manager.models
-
+
@models.setter
def models(self, value):
"""模型列表的兼容性设置器"""
self.scene_manager.models = value
-
+
# 保留gui_elements属性以兼容现有代码
@property
def gui_elements(self):
"""GUI元素列表的兼容性属性"""
return self.gui_manager.gui_elements
-
+
@gui_elements.setter
def gui_elements(self, value):
"""GUI元素列表的兼容性设置器"""
@@ -130,55 +147,55 @@ class MyWorld(CoreWorld):
def Pointlight(self,value):
self.scene_manager.Pointlight = value
-
+
# 保留guiEditMode属性以兼容现有代码
@property
def guiEditMode(self):
"""GUI编辑模式的兼容性属性"""
return self.gui_manager.guiEditMode
-
+
@guiEditMode.setter
def guiEditMode(self, value):
"""GUI编辑模式的兼容性设置器"""
self.gui_manager.guiEditMode = value
-
+
# 保留currentTool属性以兼容现有代码
@property
def currentTool(self):
"""当前工具的兼容性属性"""
return self.tool_manager.currentTool
-
+
@currentTool.setter
def currentTool(self, value):
"""当前工具的兼容性设置器"""
self.tool_manager.currentTool = value
-
+
# 保留treeWidget属性以兼容现有代码
@property
def treeWidget(self):
"""树形控件的兼容性属性"""
return self.interface_manager.treeWidget
-
+
@treeWidget.setter
def treeWidget(self, value):
"""树形控件的兼容性设置器"""
self.interface_manager.treeWidget = value
# ==================== GUI管理功能代理 ====================
-
+
# GUI元素创建方法 - 代理到gui_manager
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
"""创建2D GUI按钮"""
return self.gui_manager.createGUIButton(pos, text, size)
-
+
def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08):
"""创建2D GUI标签"""
return self.gui_manager.createGUILabel(pos, text, size)
-
+
def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08):
"""创建2D GUI文本输入框"""
return self.gui_manager.createGUIEntry(pos, placeholder, size)
-
+
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
"""创建3D空间文本"""
return self.gui_manager.createGUI3DText(pos, text, size)
@@ -190,110 +207,110 @@ class MyWorld(CoreWorld):
def createPointLight(self,pos=(20,0,5)):
"""创建点光源"""
return self.scene_manager.createPointLight(pos)
-
+
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
"""创建3D虚拟屏幕"""
return self.gui_manager.createGUIVirtualScreen(pos, size, text)
-
+
def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3):
"""创建2D GUI滑块"""
return self.gui_manager.createGUISlider(pos, text, scale)
-
+
# GUI元素管理方法 - 代理到gui_manager
def deleteGUIElement(self, gui_element):
"""删除GUI元素"""
return self.gui_manager.deleteGUIElement(gui_element)
-
+
def editGUIElement(self, gui_element, property_name, value):
"""编辑GUI元素属性"""
return self.gui_manager.editGUIElement(gui_element, property_name, value)
-
+
def duplicateGUIElement(self, gui_element):
"""复制GUI元素"""
return self.gui_manager.duplicateGUIElement(gui_element)
-
+
def editGUIElementDialog(self, gui_element):
"""显示GUI元素编辑对话框"""
return self.gui_manager.editGUIElementDialog(gui_element)
-
+
# GUI事件处理方法 - 代理到gui_manager
def onGUIButtonClick(self, button_id):
"""GUI按钮点击事件处理"""
return self.gui_manager.onGUIButtonClick(button_id)
-
+
def onGUIEntrySubmit(self, text, entry_id):
"""GUI输入框提交事件处理"""
return self.gui_manager.onGUIEntrySubmit(text, entry_id)
-
+
# GUI编辑模式方法 - 代理到gui_manager
def toggleGUIEditMode(self):
"""切换GUI编辑模式"""
return self.gui_manager.toggleGUIEditMode()
-
+
def enterGUIEditMode(self):
"""进入GUI编辑模式"""
return self.gui_manager.enterGUIEditMode()
-
+
def exitGUIEditMode(self):
"""退出GUI编辑模式"""
return self.gui_manager.exitGUIEditMode()
-
+
def createGUIEditPanel(self):
"""创建GUI编辑面板"""
return self.gui_manager.createGUIEditPanel()
-
+
def openGUIPreviewWindow(self):
"""打开独立的GUI预览窗口"""
return self.gui_manager.openGUIPreviewWindow()
-
+
def closeGUIPreviewWindow(self):
"""关闭GUI预览窗口"""
return self.gui_manager.closeGUIPreviewWindow()
-
+
# GUI工具和选择方法 - 代理到gui_manager
def setGUICreateTool(self, tool_type):
"""设置GUI创建工具"""
return self.gui_manager.setGUICreateTool(tool_type)
-
+
def deleteSelectedGUI(self):
"""删除选中的GUI元素"""
return self.gui_manager.deleteSelectedGUI()
-
+
def copySelectedGUI(self):
"""复制选中的GUI元素"""
return self.gui_manager.copySelectedGUI()
-
+
def handleGUIEditClick(self, hitPos):
"""处理GUI编辑模式下的点击"""
return self.gui_manager.handleGUIEditClick(hitPos)
-
+
def createGUIAtPosition(self, world_pos, gui_type):
"""在指定位置创建GUI元素"""
return self.gui_manager.createGUIAtPosition(world_pos, gui_type)
-
+
def findClickedGUI(self, hitNode):
"""查找被点击的GUI元素"""
return self.gui_manager.findClickedGUI(hitNode)
-
+
def selectGUIInTree(self, gui_element):
"""在树形控件中选中GUI元素"""
return self.gui_manager.selectGUIInTree(gui_element)
-
+
def updateGUISelection(self, gui_element):
"""更新GUI元素选择状态"""
return self.gui_manager.updateGUISelection(gui_element)
-
+
# GUI属性面板方法 - 代理到gui_manager
def selectGUIColor(self, gui_element):
"""选择GUI元素颜色"""
return self.gui_manager.selectGUIColor(gui_element)
-
+
def editGUI2DPosition(self, gui_element, axis, value):
"""编辑2D GUI元素位置"""
return self.gui_manager.editGUI2DPosition(gui_element, axis, value)
# ==================== 事件处理代理 ====================
-
+
def onTreeItemClicked(self, item, column):
"""处理树形控件项目点击事件 - 代理到interface_manager"""
return self.interface_manager.onTreeItemClicked(item, column)
@@ -321,7 +338,7 @@ class MyWorld(CoreWorld):
def mousePressEventMiddle(self, evt):
"""处理鼠标中键按下事件 - 代理到event_handler"""
return self.event_handler.mousePressEventMiddle(evt)
-
+
def mouseReleaseEventMiddle(self, evt):
"""处理鼠标中键释放事件 - 代理到event_handler"""
return self.event_handler.mouseReleaseEventMiddle(evt)
@@ -329,35 +346,35 @@ class MyWorld(CoreWorld):
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件 - 代理到event_handler"""
return self.event_handler.mouseMoveEvent(evt)
-
+
# ==================== 射线显示控制 ====================
-
+
def toggleRayDisplay(self):
"""切换射线显示状态 - 代理到event_handler"""
return self.event_handler.toggleRayDisplay()
-
+
def setRayDisplay(self, show=True):
"""设置射线显示状态 - 代理到event_handler"""
self.event_handler.showRay = show
if not show:
self.event_handler.clearRay()
return show
-
+
def getRayDisplay(self):
"""获取射线显示状态 - 代理到event_handler"""
return self.event_handler.showRay
-
+
def setRayLifetime(self, seconds):
"""设置射线显示时长(秒) - 代理到event_handler"""
self.event_handler.rayLifetime = seconds
print(f"射线显示时长设置为: {seconds}秒")
-
+
def getRayLifetime(self):
"""获取射线显示时长 - 代理到event_handler"""
return self.event_handler.rayLifetime
# ==================== 属性面板代理 ====================
-
+
def setPropertyLayout(self, layout):
"""设置属性面板布局引用 - 代理到property_panel"""
return self.property_panel.setPropertyLayout(layout)
@@ -369,7 +386,7 @@ class MyWorld(CoreWorld):
def updatePropertyPanel(self, item):
"""更新属性面板显示 - 代理到property_panel"""
return self.property_panel.updatePropertyPanel(item)
-
+
def updateGUIPropertyPanel(self, gui_element):
"""更新GUI元素属性面板 - 代理到property_panel"""
return self.property_panel.updateGUIPropertyPanel(gui_element)
@@ -387,7 +404,7 @@ class MyWorld(CoreWorld):
return self.tool_manager.setCurrentTool(tool)
# ==================== 场景管理功能代理 ====================
-
+
# 模型导入和处理方法 - 代理到scene_manager
def importModel(self, filepath):
"""导入模型到场景"""
@@ -396,8 +413,8 @@ class MyWorld(CoreWorld):
try:
from PyQt5.QtWidgets import QMessageBox
reply = QMessageBox.question(
- None,
- '格式转换选择',
+ None,
+ '格式转换选择',
'FBX文件检测到!\n\n是否要尝试转换为GLB格式以获得更好的动画支持?\n\n点击"是":尝试转换为GLB格式(需要安装转换工具)\n点击"否":直接使用原始FBX格式导入',
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
@@ -410,17 +427,17 @@ class MyWorld(CoreWorld):
else:
# 非FBX文件,保持原有逻辑
auto_convert = True
-
+
return self.scene_manager.importModel(filepath, auto_convert_to_glb=auto_convert)
def importModelAsync(self, filepath):
"""异步导入模型"""
return self.scene_manager.importModelAsync(filepath)
-
+
def loadAnimatedModel(self, model_path, anims=None):
"""加载带动画的模型"""
return self.scene_manager.loadAnimatedModel(model_path, anims)
-
+
# 材质和几何体处理方法 - 代理到scene_manager
def processMaterials(self, model):
"""处理模型材质"""
@@ -429,17 +446,17 @@ class MyWorld(CoreWorld):
def processModelGeometry(self, model):
"""处理模型几何体"""
return self.scene_manager.processModelGeometry(model)
-
+
# 碰撞系统方法 - 代理到scene_manager
def setupCollision(self, model):
"""为模型设置碰撞检测"""
return self.scene_manager.setupCollision(model)
-
+
# 场景树管理方法 - 代理到scene_manager
def updateSceneTree(self):
"""更新场景树显示"""
return self.scene_manager.updateSceneTree()
-
+
# 场景保存和加载方法 - 代理到scene_manager
def saveScene(self, filename):
"""保存场景到BAM文件"""
@@ -448,39 +465,39 @@ class MyWorld(CoreWorld):
def loadScene(self, filename):
"""从BAM文件加载场景"""
return self.scene_manager.loadScene(filename)
-
+
# 模型管理方法 - 代理到scene_manager
def deleteModel(self, model):
"""删除模型"""
return self.scene_manager.deleteModel(model)
-
+
def clearAllModels(self):
"""清除所有模型"""
return self.scene_manager.clearAllModels()
-
+
def getModels(self):
"""获取模型列表"""
return self.scene_manager.getModels()
-
+
def getModelCount(self):
"""获取模型数量"""
return self.scene_manager.getModelCount()
-
+
def findModelByName(self, name):
"""根据名称查找模型"""
return self.scene_manager.findModelByName(name)
# ==================== 脚本系统功能代理 ====================
-
+
# 脚本系统控制方法 - 代理到script_manager
def startScriptSystem(self):
"""启动脚本系统"""
return self.script_manager.start_system()
-
+
def stopScriptSystem(self):
"""停止脚本系统"""
return self.script_manager.stop_system()
-
+
def enableHotReload(self, enabled=True):
"""启用/禁用热重载"""
self.script_manager.hot_reload_enabled = enabled
@@ -488,153 +505,153 @@ class MyWorld(CoreWorld):
self.script_manager.start_hot_reload()
else:
self.script_manager.stop_hot_reload()
-
+
# 脚本创建和加载方法 - 代理到script_manager
def createScript(self, script_name, template="basic"):
"""创建新脚本文件"""
return self.script_manager.create_script_file(script_name, template)
-
+
def loadScript(self, script_path):
"""从文件加载脚本"""
return self.script_manager.load_script_from_file(script_path)
-
+
def loadAllScripts(self, directory=None):
"""从目录加载所有脚本"""
return self.script_manager.load_all_scripts_from_directory(directory)
-
+
def reloadScript(self, script_name):
"""重新加载脚本"""
return self.script_manager.reload_script(script_name)
-
+
# 脚本挂载和管理方法 - 代理到script_manager
def addScript(self, game_object, script_name):
"""为游戏对象添加脚本"""
return self.script_manager.add_script_to_object(game_object, script_name)
-
+
def removeScript(self, game_object, script_name):
"""从游戏对象移除脚本"""
return self.script_manager.remove_script_from_object(game_object, script_name)
-
+
def getScripts(self, game_object):
"""获取对象上的所有脚本"""
return self.script_manager.get_scripts_on_object(game_object)
-
+
def getScript(self, game_object, script_name):
"""获取对象上的特定脚本"""
return self.script_manager.get_script_on_object(game_object, script_name)
-
+
# 脚本信息查询方法 - 代理到script_manager
def getAvailableScripts(self):
"""获取所有可用的脚本名称"""
return self.script_manager.get_available_scripts()
-
+
def getScriptInfo(self, script_name):
"""获取脚本信息"""
return self.script_manager.get_script_info(script_name)
-
+
def listAllScripts(self):
"""列出所有脚本信息"""
return self.script_manager.list_all_scripts()
# ==================== VR系统功能代理 ====================
-
+
# VR系统控制方法 - 代理到vr_manager
def initializeVR(self):
"""初始化VR系统"""
return self.vr_manager.initialize_vr()
-
+
def shutdownVR(self):
"""关闭VR系统"""
return self.vr_manager.shutdown_vr()
-
+
def isVREnabled(self):
"""检查VR是否启用"""
return self.vr_manager.is_vr_enabled()
-
+
def getVRInfo(self):
"""获取VR系统信息"""
return self.vr_manager.get_vr_info()
-
+
# VR输入处理方法 - 代理到vr_input_handler
def startVRInput(self):
"""启动VR输入处理"""
return self.vr_input_handler.start_input_handling()
-
+
def stopVRInput(self):
"""停止VR输入处理"""
return self.vr_input_handler.stop_input_handling()
-
+
def showControllerRays(self, show=True):
"""显示/隐藏控制器射线"""
return self.vr_input_handler.show_controller_rays(show)
-
+
def getControllerState(self, controller_id):
"""获取控制器状态"""
return self.vr_input_handler.get_controller_state(controller_id)
-
+
def getAllControllers(self):
"""获取所有控制器"""
return self.vr_input_handler.get_all_controllers()
-
+
def setVRGestureEnabled(self, enabled):
"""设置VR手势识别启用状态"""
return self.vr_input_handler.set_gesture_enabled(enabled)
-
+
def setVRInteractionEnabled(self, enabled):
"""设置VR交互启用状态"""
return self.vr_input_handler.set_interaction_enabled(enabled)
-
+
# ALVR串流方法 - 代理到alvr_streamer
def initializeALVR(self):
"""初始化ALVR串流"""
return self.alvr_streamer.initialize()
-
+
def startALVRStreaming(self):
"""开始ALVR串流"""
return self.alvr_streamer.start_streaming()
-
+
def stopALVRStreaming(self):
"""停止ALVR串流"""
return self.alvr_streamer.stop_streaming()
-
+
def getALVRStreamingStatus(self):
"""获取ALVR串流状态"""
return self.alvr_streamer.get_streaming_status()
-
+
def setALVRStreamQuality(self, width, height, fps, bitrate):
"""设置ALVR串流质量"""
return self.alvr_streamer.set_stream_quality(width, height, fps, bitrate)
-
+
def sendHapticFeedback(self, controller_id, duration, intensity):
"""发送触觉反馈"""
return self.alvr_streamer.send_haptic_feedback(controller_id, duration, intensity)
-
+
def shutdownALVR(self):
"""关闭ALVR串流"""
return self.alvr_streamer.shutdown()
-
+
def isALVRConnected(self):
"""检查ALVR是否连接"""
return self.alvr_streamer.is_connected()
-
+
def isALVRStreaming(self):
"""检查ALVR是否在串流"""
return self.alvr_streamer.is_streaming()
-
+
# 便捷方法
def enableVRMode(self):
"""启用VR模式(一键启动)"""
print("启用VR模式...")
-
+
# 1. 初始化VR系统
if not self.initializeVR():
print("VR系统初始化失败")
return False
-
+
# 2. 启动VR输入处理
if not self.startVRInput():
print("VR输入处理启动失败")
return False
-
+
# 3. 初始化ALVR串流
if self.initializeALVR():
print("✓ ALVR串流已启用")
@@ -642,26 +659,26 @@ class MyWorld(CoreWorld):
self.startALVRStreaming()
else:
print("⚠ ALVR串流启用失败,但VR系统仍可用")
-
+
print("✓ VR模式已启用")
return True
-
+
def disableVRMode(self):
"""禁用VR模式(一键关闭)"""
print("禁用VR模式...")
-
+
# 1. 停止ALVR串流
self.stopALVRStreaming()
self.shutdownALVR()
-
+
# 2. 停止VR输入处理
self.stopVRInput()
-
+
# 3. 关闭VR系统
self.shutdownVR()
-
+
print("✓ VR模式已禁用")
-
+
def getVRStatus(self):
"""获取VR系统总体状态"""
return {
@@ -673,6 +690,23 @@ class MyWorld(CoreWorld):
"streaming_status": self.getALVRStreamingStatus() if self.isALVRConnected() else None
}
+ # 添加碰撞管理相关的代理方法
+ def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
+ """启用模型间碰撞检测"""
+ return self.collision_manager.enableModelCollisionDetection(enable, frequency, threshold)
+
+ def detectModelCollisions(self, specific_models=None, log_results=True):
+ """检测模型间碰撞"""
+ return self.collision_manager.detectModelCollisions(specific_models, log_results)
+
+ def getCollisionHistory(self, limit=None):
+ """获取碰撞历史"""
+ return self.collision_manager.getCollisionHistory(limit)
+
+ def getCollisionStatistics(self):
+ """获取碰撞统计"""
+ return self.collision_manager.getCollisionStatistics()
+
# ==================== 项目管理功能代理 ====================
# 以下函数代理到project_manager模块的对应功能
diff --git a/scene/scene_manager.py b/scene/scene_manager.py
index 75671fe2..fadbd002 100644
--- a/scene/scene_manager.py
+++ b/scene/scene_manager.py
@@ -7,6 +7,8 @@
"""
import os
+
+from PyQt5.QtCore import Qt
from panda3d.core import (
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere,
@@ -33,13 +35,12 @@ class SceneManager:
self.Spotlight = []
self.Pointlight = []
-
print("✓ 场景管理系统初始化完成")
-
+
# ==================== 模型导入和处理 ====================
def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
- """导入模型到场景
+ """导入模型到场景 - 只在根节点下创建
Args:
filepath: 模型文件路径
@@ -48,14 +49,16 @@ class SceneManager:
auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持
"""
try:
- print(f"\n=== 开始导入模型: {filepath} ===")
+ print(f"\n💾 开始导入模型: {os.path.basename(filepath)}")
print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}")
+ print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}")
print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}")
+ # 预处理文件路径和转换
filepath = util.normalize_model_path(filepath)
original_filepath = filepath
- # 检查是否需要转换为GLB以获得更好的动画支持
+ # 检查是否需要转换为GLB
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
print(f"🔄 检测到需要转换的格式,尝试转换为GLB...")
converted_path = self._convertToGLBWithProgress(filepath)
@@ -72,70 +75,95 @@ class SceneManager:
pass
else:
print(f"⚠️ 转换失败,使用原始文件")
+
+ # 直接在render根节点下创建模型
+ print("--- 在根节点下创建模型实例 ---")
- # 总是重新加载模型以确保材质信息完整
- # 不使用ModelPool缓存,避免材质信息丢失问题
+ # 加载模型
print("直接从文件加载模型...")
model = self.world.loader.loadModel(filepath)
if not model:
- print("加载模型失败")
+ print("❌ 加载模型失败")
return None
# 设置模型名称
model_name = os.path.basename(filepath)
model.setName(model_name)
- # 将模型添加到场景
+ # 将模型挂载到render根节点
model.reparentTo(self.world.render)
- # 保存原始路径和转换后的路径
+
+ # 设置标签和路径信息
model.setTag("model_path", filepath)
model.setTag("original_path", original_filepath)
+ model.setTag("file", model.getName())
+ model.setTag("is_model_root", "1")
+ model.setTag("is_scene_element", "1")
+ model.setTag("created_by_user", "1")
+
if filepath != original_filepath:
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
model.setTag("converted_to_glb", "true")
- # 可选的单位转换(主要针对FBX)
+ # 应用处理选项
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
print("应用FBX单位转换(厘米到米)...")
self._applyUnitConversion(model, 0.01)
+ model.setTag("unit_conversion_applied", "true")
- # 智能缩放标准化(处理FBX子节点的大缩放值)
if normalize_scales and filepath.lower().endswith('.fbx'):
print("标准化FBX模型缩放层级...")
self._normalizeModelScales(model)
+ model.setTag("scale_normalization_applied", "true")
# 调整模型位置到地面
self._adjustModelToGround(model)
# 创建并设置基础材质
- print("\n=== 开始设置材质 ===")
+ print("设置材质...")
self._applyMaterialsToModel(model)
# 设置碰撞检测(重要!用于选择功能)
- print("\n=== 设置碰撞检测 ===")
+ print("设置碰撞检测...")
self.setupCollision(model)
- # 添加文件标签用于保存/加载
- model.setTag("file", model_name)
- model.setTag("is_model_root", "1")
-
- # 记录应用的处理选项
- if apply_unit_conversion:
- model.setTag("unit_conversion_applied", "true")
- if normalize_scales:
- model.setTag("scale_normalization_applied", "true")
-
# 添加到模型列表
self.models.append(model)
-
- # 更新场景树
- self.updateSceneTree()
-
- print(f"=== 模型导入成功: {model_name} ===\n")
+
+ print(f"✅ 创建模型成功: {model.getName()}")
+
+ # 获取树形控件并添加到Qt树中
+ tree_widget = self._get_tree_widget()
+ if tree_widget:
+ # 找到根节点项
+ root_item = None
+ for i in range(tree_widget.topLevelItemCount()):
+ item = tree_widget.topLevelItem(i)
+ if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
+ root_item = item
+ break
+
+ if root_item:
+ qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE")
+ if qt_item:
+ tree_widget.setCurrentItem(qt_item)
+ # 更新选择和属性面板
+ tree_widget.update_selection_and_properties(model, qt_item)
+ print("✅ Qt树节点添加成功")
+ else:
+ print("⚠️ Qt树节点添加失败,但Panda3D对象已创建")
+ else:
+ print("⚠️ 未找到根节点项,无法添加到Qt树")
+ else:
+ print("⚠️ 无法访问树形控件")
+
+ print(f"🎉 模型导入完成")
return model
-
+
except Exception as e:
- print(f"导入模型失败: {str(e)}")
+ print(f"❌ 导入模型过程失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
return None
# def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True):
@@ -196,22 +224,22 @@ class SceneManager:
# import traceback
# traceback.print_exc()
# return None
-
+
def _applyMaterialsToModel(self, model):
"""递归应用材质到模型的所有GeomNode"""
def apply_material(node_path, depth=0):
indent = " " * depth
print(f"{indent}处理节点: {node_path.getName()}")
print(f"{indent}节点类型: {node_path.node().__class__.__name__}")
-
+
if isinstance(node_path.node(), GeomNode):
print(f"{indent}发现GeomNode,处理材质")
geom_node = node_path.node()
-
+
# 检查所有几何体的状态
has_color = False
color = None
-
+
# 首先检查节点自身的状态
node_state = node_path.getState()
if node_state.hasAttrib(MaterialAttrib.getClassType()):
@@ -221,20 +249,20 @@ class SceneManager:
color = node_material.getDiffuse()
has_color = True
print(f"{indent}从节点材质获取颜色: {color}")
-
+
# 检查FBX特有的属性
for tag_key in node_path.getTagKeys():
print(f"{indent}发现标签: {tag_key}")
if "color" in tag_key.lower() or "diffuse" in tag_key.lower():
tag_value = node_path.getTag(tag_key)
print(f"{indent}颜色相关标签: {tag_key} = {tag_value}")
-
+
# 如果还没找到颜色,检查几何体
if not has_color:
for i in range(geom_node.getNumGeoms()):
geom = geom_node.getGeom(i)
state = geom_node.getGeomState(i)
-
+
# 检查顶点颜色
vdata = geom.getVertexData()
format = vdata.getFormat()
@@ -245,7 +273,7 @@ class SceneManager:
if "color" in column_name.lower():
print(f"{indent}发现顶点颜色数据: {column_name}")
# 这里可以读取顶点颜色,但先记录发现
-
+
# 检查材质属性
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
@@ -261,7 +289,7 @@ class SceneManager:
has_color = True
print(f"{indent}从漫反射颜色获取: {color}")
break
-
+
# 检查颜色属性
if not has_color and state.hasAttrib(ColorAttrib.getClassType()):
color_attrib = state.getAttrib(ColorAttrib.getClassType())
@@ -270,7 +298,7 @@ class SceneManager:
has_color = True
print(f"{indent}从颜色属性获取: {color}")
break
-
+
# 创建新材质
material = Material()
if has_color:
@@ -281,7 +309,7 @@ class SceneManager:
else:
print(f"{indent}使用默认颜色")
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
-
+
# 设置其他材质属性
material.setAmbient((0.2, 0.2, 0.2, 1.0))
material.setSpecular((0.5, 0.5, 0.5, 1.0))
@@ -292,72 +320,72 @@ class SceneManager:
# 应用材质
node_path.setMaterial(material)
print(f"{indent}几何体数量: {geom_node.getNumGeoms()}")
-
+
# 递归处理子节点
child_count = node_path.getNumChildren()
print(f"{indent}子节点数量: {child_count}")
for i in range(child_count):
child = node_path.getChild(i)
apply_material(child, depth + 1)
-
+
# 应用材质
print("\n开始递归应用材质...")
apply_material(model)
print("=== 材质设置完成 ===\n")
-
+
def _adjustModelToGround(self, model):
"""智能调整模型到地面,但保持原有缩放结构"""
try:
print("调整模型位置到地面...")
-
+
# 获取模型的边界框
bounds = model.getBounds()
if not bounds or bounds.isEmpty():
print("无法获取模型边界,使用默认位置")
model.setPos(0, 0, 0)
return
-
+
# 获取边界框的最低点
min_point = bounds.getMin()
center = bounds.getCenter()
-
+
# 计算需要移动的距离,使模型底部贴合地面(Z=0)
# 这里不涉及缩放,只是简单的位置调整
ground_offset = -min_point.getZ()
-
+
# 设置模型位置:X,Y居中,Z调整到地面
model.setPos(0, 0, ground_offset)
-
+
print(f"模型边界: 最小点{min_point}, 中心{center}")
print(f"地面偏移: {ground_offset}")
print(f"最终位置: {model.getPos()}")
-
+
except Exception as e:
print(f"调整模型位置失败: {str(e)}")
# 失败时使用默认位置
model.setPos(0, 0, 0)
-
+
def _applyUnitConversion(self, model, scale_factor):
"""应用单位转换缩放
-
+
Args:
model: 要转换的模型
scale_factor: 缩放因子(如0.01表示从厘米转换到米)
"""
try:
print(f"应用单位转换缩放: {scale_factor}")
-
+
# 检查模型是否已经应用过单位转换
if model.hasTag("unit_conversion_applied"):
print("模型已应用过单位转换,跳过")
return
-
+
# 获取当前边界用于后续位置调整
original_bounds = model.getBounds()
-
+
# 应用缩放
model.setScale(scale_factor)
-
+
# 重新调整位置(因为缩放会影响边界)
if original_bounds and not original_bounds.isEmpty():
new_bounds = model.getBounds()
@@ -365,52 +393,52 @@ class SceneManager:
ground_offset = -min_point.getZ()
model.setZ(ground_offset)
print(f"缩放后重新调整位置: Z偏移 = {ground_offset}")
-
+
print(f"单位转换完成,缩放因子: {scale_factor}")
-
+
except Exception as e:
print(f"应用单位转换失败: {str(e)}")
-
+
def _normalizeModelScales(self, model):
"""智能标准化模型缩放层级
-
+
检测并修复FBX模型中子节点的大缩放值问题
"""
try:
print("开始分析模型缩放结构...")
-
+
# 收集所有节点的缩放信息
scale_info = []
self._collectScaleInfo(model, scale_info)
-
+
if not scale_info:
print("没有找到需要处理的缩放信息")
return
-
+
# 分析缩放模式
large_scales = [info for info in scale_info if max(abs(info['scale'].x), abs(info['scale'].y), abs(info['scale'].z)) > 10]
-
+
if not large_scales:
print("没有发现大缩放值,无需标准化")
return
-
+
print(f"发现 {len(large_scales)} 个节点有大缩放值")
-
+
# 计算标准化因子(基于最常见的大缩放值)
common_large_scale = self._findCommonLargeScale(large_scales)
if common_large_scale:
normalize_factor = 1.0 / common_large_scale
print(f"检测到常见大缩放值: {common_large_scale}, 标准化因子: {normalize_factor}")
-
+
# 应用标准化
self._applyScaleNormalization(model, normalize_factor)
print("✓ 缩放标准化完成")
else:
print("无法确定合适的标准化因子,跳过标准化")
-
+
except Exception as e:
print(f"缩放标准化失败: {str(e)}")
-
+
def _collectScaleInfo(self, node, scale_info, depth=0):
"""递归收集节点缩放信息"""
try:
@@ -421,15 +449,15 @@ class SceneManager:
'scale': scale,
'depth': depth
})
-
+
# 递归处理子节点
for i in range(node.getNumChildren()):
child = node.getChild(i)
self._collectScaleInfo(child, scale_info, depth + 1)
-
+
except Exception as e:
print(f"收集缩放信息失败 ({node.getName()}): {str(e)}")
-
+
def _findCommonLargeScale(self, large_scales):
"""找到最常见的大缩放值"""
try:
@@ -439,66 +467,66 @@ class SceneManager:
scale = info['scale']
max_scale = max(abs(scale.x), abs(scale.y), abs(scale.z))
scale_values.append(round(max_scale)) # 四舍五入到整数
-
+
if not scale_values:
return None
-
+
# 找到最常见的值
from collections import Counter
counter = Counter(scale_values)
most_common = counter.most_common(1)[0]
-
+
print(f"缩放值统计: {dict(counter)}")
print(f"最常见的大缩放值: {most_common[0]} (出现{most_common[1]}次)")
-
+
# 只有当最常见的值确实很大时才返回
if most_common[0] >= 10:
return float(most_common[0])
-
+
return None
-
+
except Exception as e:
print(f"分析常见缩放值失败: {str(e)}")
return None
-
+
def _applyScaleNormalization(self, node, normalize_factor, depth=0):
"""递归应用缩放标准化,同时调整位置以保持视觉一致性"""
try:
indent = " " * depth
current_scale = node.getScale()
current_pos = node.getPos()
-
+
# 检查是否需要标准化(只处理明显的大缩放)
max_scale_component = max(abs(current_scale.x), abs(current_scale.y), abs(current_scale.z))
-
+
if max_scale_component > 10: # 只标准化明显的大缩放
# 应用新的缩放
new_scale = current_scale * normalize_factor
node.setScale(new_scale)
-
+
# 同时调整位置:当缩放变小时,位置也应该相应变小以保持视觉相对位置
# 这确保了子节点之间的相对距离在视觉上保持一致
new_pos = current_pos * normalize_factor
node.setPos(new_pos)
-
+
print(f"{indent}标准化 {node.getName()}:")
print(f"{indent} 缩放: {current_scale} -> {new_scale}")
print(f"{indent} 位置: {current_pos} -> {new_pos}")
-
+
# 递归处理子节点
for i in range(node.getNumChildren()):
child = node.getChild(i)
self._applyScaleNormalization(child, normalize_factor, depth + 1)
-
+
except Exception as e:
print(f"应用缩放标准化失败 ({node.getName()}): {str(e)}")
-
+
def importModelAsync(self, filepath):
"""异步导入模型"""
try:
# 创建异步加载请求
request = self.world.loader.makeAsyncRequest(filepath)
-
+
# 添加完成回调
def modelLoaded(task):
if task.isReady():
@@ -507,15 +535,15 @@ class SceneManager:
# 处理加载完成的模型
self.processLoadedModel(model)
return task.done()
-
+
request.done_event = modelLoaded
-
+
# 开始异步加载
self.world.loader.loadAsync(request)
-
+
except Exception as e:
print(f"异步加载模型失败: {str(e)}")
-
+
def loadAnimatedModel(self, model_path, anims=None, auto_play=True):
"""加载带动画的模型
@@ -570,7 +598,7 @@ class SceneManager:
return None
# ==================== 材质和几何体处理 ====================
-
+
def processMaterials(self, model):
"""处理模型材质"""
if isinstance(model.node(), GeomNode):
@@ -580,7 +608,7 @@ class SceneManager:
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
material.setSpecular((0.5, 0.5, 0.5, 1.0))
material.setShininess(32.0)
-
+
# 检查FBX材质
state = model.node().getGeomState(0)
if state.hasAttrib(MaterialAttrib.getClassType()):
@@ -591,65 +619,106 @@ class SceneManager:
material.setDiffuse(fbx_material.getDiffuse())
material.setSpecular(fbx_material.getSpecular())
material.setShininess(fbx_material.getShininess())
-
+
# 应用材质
model.setMaterial(material)
-
+
def processModelGeometry(self, model):
"""处理模型几何体"""
# 创建EggData对象
egg_data = EggData()
-
+
# 处理顶点数据
vertex_pool = EggVertexPool("vpool")
egg_data.addChild(vertex_pool)
-
+
# 处理几何体
if isinstance(model.node(), GeomNode):
for i in range(model.node().getNumGeoms()):
geom = model.node().getGeom(i)
# 处理几何体数据
# ...
-
+
# ==================== 碰撞系统 ====================
-
+
def setupCollision(self, model):
- """为模型设置碰撞检测"""
- # 创建碰撞节点
- cNode = CollisionNode(f'modelCollision_{model.getName()}')
- # 设置碰撞掩码
- cNode.setIntoCollideMask(BitMask32.bit(2)) # 使用第2位作为模型的碰撞掩码
-
- # 获取模型的边界
- bounds = model.getBounds()
- center = bounds.getCenter()
- radius = bounds.getRadius()
-
- # 添加碰撞球体
- cSphere = CollisionSphere(center, radius)
- cNode.addSolid(cSphere)
-
- # 将碰撞节点附加到模型上
- cNodePath = model.attachNewNode(cNode)
- #cNodePath.hide()
- # cNodePath.show() # 取消注释可以显示碰撞体,用于调试
-
+ """为模型设置碰撞检测(增强版本)"""
+ try:
+ # 创建碰撞节点
+ cNode = CollisionNode(f'modelCollision_{model.getName()}')
+
+ # 设置碰撞掩码
+ cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择
+
+ # 如果启用了模型间碰撞检测,添加额外的掩码
+ if (hasattr(self.world, 'collision_manager') and
+ self.world.collision_manager.model_collision_enabled):
+ # 同时设置模型间碰撞掩码
+ current_mask = cNode.getIntoCollideMask()
+ model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION
+ cNode.setIntoCollideMask(current_mask | model_collision_mask)
+ print(f"为 {model.getName()} 启用模型间碰撞检测")
+
+ # 获取模型的边界
+ bounds = model.getBounds()
+ if bounds.isEmpty():
+ print(f"⚠️ 模型 {model.getName()} 边界为空,使用默认碰撞体")
+ # 使用默认的小球体
+ cSphere = CollisionSphere(Point3(0, 0, 0), 1.0)
+ else:
+ center = bounds.getCenter()
+ radius = bounds.getRadius()
+
+ # 确保半径不为零
+ if radius <= 0:
+ radius = 1.0
+ print(f"⚠️ 模型 {model.getName()} 半径为零,使用默认半径 1.0")
+ #
+ # # 添加碰撞球体
+ # cSphere = CollisionSphere(center, radius)
+ cSphere = self.world.collision_manager.createCollisionShape(model, 'polygon')
+
+ cNode.addSolid(cSphere)
+
+ # 将碰撞节点附加到模型上
+ cNodePath = model.attachNewNode(cNode)
+
+ # 根据调试设置决定是否显示碰撞体
+ if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
+ cNodePath.show()
+ else:
+ cNodePath.hide()
+
+ # 为模型添加碰撞相关标签
+ model.setTag("has_collision", "true")
+ model.setTag("collision_radius", str(radius if 'radius' in locals() else 1.0))
+
+ print(f"✅ 为模型 {model.getName()} 设置碰撞检测完成")
+
+ return cNodePath
+
+ except Exception as e:
+ print(f"❌ 为模型 {model.getName()} 设置碰撞检测失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
+
# ==================== 场景树管理 ====================
-
+
def updateSceneTree(self):
"""更新场景树显示 - 代理到interface_manager"""
if hasattr(self.world, 'interface_manager'):
return self.world.interface_manager.updateSceneTree()
else:
print("界面管理器未初始化,无法更新场景树")
-
+
# ==================== 场景保存和加载 ====================
-
+
def saveScene(self, filename):
"""保存场景到BAM文件"""
try:
print(f"\n=== 开始保存场景到: {filename} ===")
-
+
# 遍历所有模型,保存材质状态和变换信息
for model in self.models:
# 保存变换信息(关键!)
@@ -660,10 +729,10 @@ class SceneManager:
print(f" 位置: {model.getPos()}")
print(f" 旋转: {model.getHpr()}")
print(f" 缩放: {model.getScale()}")
-
+
# 获取当前状态
state = model.getState()
-
+
# 如果有材质属性,保存为标签
if state.hasAttrib(MaterialAttrib.getClassType()):
mat_attrib = state.getAttrib(MaterialAttrib.getClassType())
@@ -677,8 +746,8 @@ class SceneManager:
model.setTag("material_shininess", str(material.getShininess()))
if material.hasBaseColor():
model.setTag("material_basecolor", str(material.getBaseColor()))
-
- # 如果有颜色属性,保存为标签
+
+ # 如果有颜色属性,保存为标签
if state.hasAttrib(ColorAttrib.getClassType()):
color_attrib = state.getAttrib(ColorAttrib.getClassType())
if not color_attrib.isOff():
@@ -691,53 +760,53 @@ class SceneManager:
except Exception as e:
print(f"保存场景时发生错误: {str(e)}")
return False
-
+
def loadScene(self, filename):
"""从BAM文件加载场景"""
try:
print(f"\n=== 开始加载场景: {filename} ===")
-
+
# 清除当前场景
print("\n清除当前场景...")
for model in self.models:
model.removeNode()
self.models.clear()
-
+
# 加载场景
scene = self.world.loader.loadModel(filename)
if not scene:
return False
-
+
# 遍历场景中的所有模型节点
def processNode(nodePath, depth=0):
indent = " " * depth
print(f"{indent}处理节点: {nodePath.getName()}")
-
+
# 跳过render节点的递归
if nodePath.getName() == "render" and depth > 0:
print(f"{indent}跳过重复的render节点")
return
-
+
# 跳过光源节点
if nodePath.getName() in ["alight", "dlight"]:
print(f"{indent}跳过光源节点: {nodePath.getName()}")
return
-
+
# 跳过相机节点
if nodePath.getName() in ["camera", "cam"]:
print(f"{indent}跳过相机节点: {nodePath.getName()}")
return
-
+
if isinstance(nodePath.node(), ModelRoot):
print(f"{indent}找到模型根节点!")
-
+
# 清除现有材质状态
nodePath.clearMaterial()
nodePath.clearColor()
-
+
# 创建新材质
material = Material()
-
+
# 从标签恢复材质属性
def parseColor(color_str):
"""解析颜色字符串为Vec4"""
@@ -748,7 +817,7 @@ class SceneManager:
return Vec4(r, g, b, a)
except:
return Vec4(1, 1, 1, 1) # 默认白色
-
+
if nodePath.hasTag("material_ambient"):
material.setAmbient(parseColor(nodePath.getTag("material_ambient")))
if nodePath.hasTag("material_diffuse"):
@@ -761,14 +830,14 @@ class SceneManager:
material.setShininess(float(nodePath.getTag("material_shininess")))
if nodePath.hasTag("material_basecolor"):
material.setBaseColor(parseColor(nodePath.getTag("material_basecolor")))
-
+
# 应用材质
nodePath.setMaterial(material)
-
+
# 恢复颜色属性
if nodePath.hasTag("color"):
nodePath.setColor(parseColor(nodePath.getTag("color")))
-
+
# 恢复变换信息(关键!)
def parseVec3(vec_str):
"""解析向量字符串为Vec3"""
@@ -780,52 +849,52 @@ class SceneManager:
except Exception as e:
print(f"解析向量失败: {vec_str}, 错误: {e}")
return Vec3(0, 0, 0) # 默认值
-
+
if nodePath.hasTag("transform_pos"):
pos = parseVec3(nodePath.getTag("transform_pos"))
nodePath.setPos(pos)
print(f"{indent}恢复位置: {pos}")
-
+
if nodePath.hasTag("transform_hpr"):
hpr = parseVec3(nodePath.getTag("transform_hpr"))
nodePath.setHpr(hpr)
print(f"{indent}恢复旋转: {hpr}")
-
+
if nodePath.hasTag("transform_scale"):
scale = parseVec3(nodePath.getTag("transform_scale"))
nodePath.setScale(scale)
print(f"{indent}恢复缩放: {scale}")
-
+
# 将模型重新挂载到render下
nodePath.wrtReparentTo(self.world.render)
-
+
# 为加载的模型设置碰撞检测
self.setupCollision(nodePath)
-
+
self.models.append(nodePath)
-
+
# 递归处理子节点
for child in nodePath.getChildren():
processNode(child, depth + 1)
-
+
print("\n开始处理场景节点...")
processNode(scene)
-
+
# 移除临时场景节点
scene.removeNode()
-
+
# 更新场景树
self.updateSceneTree()
-
+
print("=== 场景加载完成 ===\n")
return True
-
+
except Exception as e:
print(f"加载场景时发生错误: {str(e)}")
return False
-
+
# ==================== 模型管理 ====================
-
+
def deleteModel(self, model):
"""删除模型"""
try:
@@ -890,99 +959,489 @@ class SceneManager:
print(f"异步加载模型完成: {model.getName()}")
def createSpotLight(self, pos=(0, 0, 0)):
- from RenderPipelineFile.rpcore import SpotLight, RenderPipeline
- from panda3d.core import Vec3,NodePath
+ """创建聚光灯 - 支持多选创建,优化版本"""
+ try:
+ from RenderPipelineFile.rpcore import SpotLight
+ from QPanda3D.Panda3DWorld import get_render_pipeline
+ from panda3d.core import Vec3, NodePath
+ from PyQt5.QtCore import Qt
- render_pipeline = get_render_pipeline()
+ print(f"🔆 开始创建聚光灯,位置: {pos}")
- # 创建一个挂载节点(你控制的)
- light_np = NodePath("SpotlightAttachNode")
- light_np.reparentTo(self.world.render)
- #light_np.setPos(*pos)
+ # 获取树形控件
+ tree_widget = self._get_tree_widget()
+ if not tree_widget:
+ print("❌ 无法访问树形控件")
+ return None
- self.half_energy = 5000
- self.lamp_fov = 70
- self.lamp_radius = 1000
+ # 获取目标父节点列表
+ target_parents = tree_widget.get_target_parents_for_creation()
+ if not target_parents:
+ print("❌ 没有找到有效的父节点")
+ return None
- light = SpotLight()
- light.direction = Vec3(0, 0, -1) # 光照方向
- light.fov = self.lamp_fov # 光源角度(类似手电筒)
- light.set_color_from_temperature(5 * 1000.0) # 色温(K)
- light.energy = self.half_energy # 光照强度
- light.radius = self.lamp_radius # 影响范围
- light.casts_shadows = True # 是否投射阴影
- light.shadow_map_resolution = 256 # 阴影分辨率
- light.setPos(*pos)
- #light_np.setPos(*pos)
+ created_lights = []
+ render_pipeline = get_render_pipeline()
- #light_np = render_pipeline.add_light(light, parent=self.world.render)
- render_pipeline.add_light(light) # 添加到渲染管线
+ # 为每个有效的父节点创建聚光灯
+ for parent_item, parent_node in target_parents:
+ try:
+ # 生成唯一名称
+ light_name = f"Spotlight_{len(self.Spotlight)}"
- light_name = f"Spotlight_{len(self.Spotlight)}"
+ # 创建挂载节点 - 挂载到选中的父节点
+ light_np = NodePath(light_name)
+ light_np.reparentTo(parent_node) # 挂载到父节点而不是render
+ light_np.setPos(*pos)
- light_np.setName(light_name) # 设置唯一名称
- #light_np.reparentTo(self.world.render) # 挂载到场景根节点
+ # 创建聚光灯对象
+ light = SpotLight()
+ light.direction = Vec3(0, 0, -1)
+ light.fov = 70
+ light.set_color_from_temperature(5 * 1000.0)
+ light.energy = 5000
+ light.radius = 1000
+ light.casts_shadows = True
+ light.shadow_map_resolution = 256
- light_np.setTag("light_type", "spot_light")
- light_np.setTag("is_scene_element", "1")
- light_np.setTag("light_energy", str(light.energy))
+ # 设置光源的世界坐标位置
+ world_pos = light_np.getPos(self.world.render)
+ light.setPos(world_pos)
- light_np.setPythonTag("rp_light_object", light)
+ # 添加到渲染管线
+ render_pipeline.add_light(light)
- self.Spotlight.append(light_np)
+ # 设置节点属性和标签
+ light_np.setTag("light_type", "spot_light")
+ light_np.setTag("is_scene_element", "1")
+ light_np.setTag("light_energy", str(light.energy))
+ light_np.setTag("created_by_user", "1")
- if hasattr(self.world, 'updateSceneTree'):
- self.world.updateSceneTree()
+ # 保存光源对象引用
+ light_np.setPythonTag("rp_light_object", light)
- #print("nikan"+light_np.getHpr())
+ # 添加到管理列表
+ self.Spotlight.append(light_np)
+
+ print(f"✅ 为 {parent_item.text(0)} 创建聚光灯成功: {light_name}")
+
+ # 在Qt树形控件中添加对应节点
+ qt_item = tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE")
+ if qt_item:
+ created_lights.append((light_np, qt_item))
+ else:
+ created_lights.append((light_np, None))
+ print("⚠️ Qt树节点添加失败,但Panda3D对象已创建")
+
+ except Exception as e:
+ print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}")
+ continue
+
+ # 处理创建结果
+ if not created_lights:
+ print("❌ 没有成功创建任何聚光灯")
+ return None
+
+ # 选中最后创建的光源
+ if created_lights:
+ last_light_np, last_qt_item = created_lights[-1]
+ if last_qt_item:
+ tree_widget.setCurrentItem(last_qt_item)
+ # 更新选择和属性面板
+ tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
+
+ print(f"🎉 总共创建了 {len(created_lights)} 个聚光灯")
+
+ # 返回值处理
+ if len(created_lights) == 1:
+ return created_lights[0][0] # 单个光源返回NodePath
+ else:
+ return [light_np for light_np, _ in created_lights] # 多个光源返回列表
+
+ except Exception as e:
+ print(f"❌ 创建聚光灯过程失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
def createPointLight(self, pos=(0, 0, 0)):
- from RenderPipelineFile.rpcore import PointLight, RenderPipeline
- from panda3d.core import Vec3, NodePath
+ """创建点光源 - 支持多选创建,优化版本"""
+ try:
+ from RenderPipelineFile.rpcore import PointLight
+ from QPanda3D.Panda3DWorld import get_render_pipeline
+ from panda3d.core import Vec3, NodePath
+ from PyQt5.QtCore import Qt
- render_pipeline = get_render_pipeline()
+ print(f"💡 开始创建点光源,位置: {pos}")
- # 创建一个挂载节点(你控制的)
- light_np = NodePath("PointlightAttachNode")
- light_np.reparentTo(self.world.render)
+ # 获取树形控件
+ tree_widget = self._get_tree_widget()
+ if not tree_widget:
+ print("❌ 无法访问树形控件")
+ return None
+ # 获取目标父节点列表
+ target_parents = tree_widget.get_target_parents_for_creation()
+ if not target_parents:
+ print("❌ 没有找到有效的父节点")
+ return None
- light = PointLight()
- light.setPos(*pos)
- light_np.setPos(*pos)
- light.energy = 5000
- light.radius = 1000
- light.inner_radius = 0.4
- light.set_color_from_temperature(5 * 1000.0) # 色温(K)
- light.casts_shadows = True # 是否投射阴影
- light.shadow_map_resolution = 256 # 阴影分辨率
+ created_lights = []
+ render_pipeline = get_render_pipeline()
- render_pipeline.add_light(light) # 添加到渲染管线
+ # 为每个有效的父节点创建点光源
+ for parent_item, parent_node in target_parents:
+ try:
+ # 生成唯一名称
+ light_name = f"Pointlight_{len(self.Pointlight)}"
- light_name = f"Pointlight{len(self.Pointlight)}"
+ # 创建挂载节点 - 挂载到选中的父节点
+ light_np = NodePath(light_name)
+ light_np.reparentTo(parent_node) # 挂载到父节点而不是render
+ light_np.setPos(*pos)
- light_np.setName(light_name) # 设置唯一名称
+ # 创建点光源对象
+ light = PointLight()
- #light_np = NodePath(f"PointLight_{len(self.Pointlight)}")
- #light_np.reparentTo(self.world.render)
- #light_np.setPos(*pos)
+ # 设置光源的世界坐标位置
+ world_pos = light_np.getPos(self.world.render)
+ light.setPos(world_pos)
- light_np.setTag("light_type", "point_light")
- light_np.setTag("is_scene_element", "1")
- light_np.setTag("light_energy", str(light.energy))
+ light.energy = 5000
+ light.radius = 1000
+ light.inner_radius = 0.4
+ light.set_color_from_temperature(5 * 1000.0)
+ light.casts_shadows = True
+ light.shadow_map_resolution = 256
- # 保存光源对象引用(重要!用于属性面板)
- light_np.setPythonTag("rp_light_object", light)
+ # 添加到渲染管线
+ render_pipeline.add_light(light)
- self.Pointlight.append(light_np)
+ # 设置节点属性和标签
+ light_np.setTag("light_type", "point_light")
+ light_np.setTag("is_scene_element", "1")
+ light_np.setTag("light_energy", str(light.energy))
+ light_np.setTag("created_by_user", "1")
- if hasattr(self.world, 'updateSceneTree'):
- self.world.updateSceneTree()
+ # 保存光源对象引用
+ light_np.setPythonTag("rp_light_object", light)
- return light,light_np
+ # 添加到管理列表
+ self.Pointlight.append(light_np)
+
+ print(f"✅ 为 {parent_item.text(0)} 创建点光源成功: {light_name}")
+
+ # 在Qt树形控件中添加对应节点
+ qt_item =tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE")
+ if qt_item:
+ created_lights.append((light_np, qt_item))
+ else:
+ created_lights.append((light_np, None))
+ print("⚠️ Qt树节点添加失败,但Panda3D对象已创建")
+
+ except Exception as e:
+ print(f"❌ 为 {parent_item.text(0)} 创建点光源失败: {str(e)}")
+ continue
+
+ # 处理创建结果
+ if not created_lights:
+ print("❌ 没有成功创建任何点光源")
+ return None
+
+ # 选中最后创建的光源
+ if created_lights:
+ last_light_np, last_qt_item = created_lights[-1]
+ if last_qt_item:
+ tree_widget.setCurrentItem(last_qt_item)
+ # 更新选择和属性面板
+ tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
+
+ print(f"🎉 总共创建了 {len(created_lights)} 个点光源")
+
+ # 返回值处理
+ if len(created_lights) == 1:
+ return created_lights[0][0] # 单个光源返回NodePath
+ else:
+ return [light_np for light_np, _ in created_lights] # 多个光源返回列表
+
+ except Exception as e:
+ print(f"❌ 创建点光源过程失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
+
+ def _get_tree_widget(self):
+ """安全获取树形控件"""
+ try:
+ if (hasattr(self.world, 'interface_manager') and
+ hasattr(self.world.interface_manager, 'treeWidget')):
+ return self.world.interface_manager.treeWidget
+ except AttributeError:
+ pass
+ return None
+
+ def _importModelSingle(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
+ """传统单一模型导入方法(兼容性保留)"""
+ try:
+ print(f"\n=== 使用传统模式导入模型: {filepath} ===")
+
+ filepath = util.normalize_model_path(filepath)
+ original_filepath = filepath
+
+ # 检查是否需要转换为GLB
+ if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
+ print(f"🔄 检测到需要转换的格式,尝试转换为GLB...")
+ converted_path = self._convertToGLBWithProgress(filepath)
+ if converted_path:
+ print(f"✅ 转换成功: {converted_path}")
+ filepath = converted_path
+ try:
+ from PyQt5.QtWidgets import QMessageBox
+ original_ext = os.path.splitext(original_filepath)[1].upper()
+ QMessageBox.information(None, "转换成功",
+ f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!")
+ except:
+ pass
+ else:
+ print(f"⚠️ 转换失败,使用原始文件")
+
+ # 加载模型
+ print("直接从文件加载模型...")
+ model = self.world.loader.loadModel(filepath)
+ if not model:
+ print("加载模型失败")
+ return None
+
+ # 设置模型名称
+ model_name = os.path.basename(filepath)
+ model.setName(model_name)
+
+ # 将模型添加到场景
+ model.reparentTo(self.world.render)
+
+ # 设置标签和路径信息
+ model.setTag("model_path", filepath)
+ model.setTag("original_path", original_filepath)
+ model.setTag("file", model_name)
+ model.setTag("is_model_root", "1")
+ model.setTag("is_scene_element", "1")
+ model.setTag("created_by_user", "1")
+
+ if filepath != original_filepath:
+ model.setTag("converted_from", os.path.splitext(original_filepath)[1])
+ model.setTag("converted_to_glb", "true")
+
+ # 应用处理选项
+ if apply_unit_conversion and filepath.lower().endswith('.fbx'):
+ print("应用FBX单位转换(厘米到米)...")
+ self._applyUnitConversion(model, 0.01)
+ model.setTag("unit_conversion_applied", "true")
+
+ if normalize_scales and filepath.lower().endswith('.fbx'):
+ print("标准化FBX模型缩放层级...")
+ self._normalizeModelScales(model)
+ model.setTag("scale_normalization_applied", "true")
+
+ # 调整模型位置到地面
+ self._adjustModelToGround(model)
+
+ # 创建并设置基础材质
+ print("\n=== 开始设置材质 ===")
+ self._applyMaterialsToModel(model)
+
+ # 设置碰撞检测(重要!用于选择功能)
+ print("\n=== 设置碰撞检测 ===")
+ self.setupCollision(model)
+
+ # 添加到模型列表
+ self.models.append(model)
+
+ # 更新场景树
+ self.updateSceneTree()
+
+ print(f"=== 模型导入成功: {model_name} ===\n")
+ return model
+
+ except Exception as e:
+ print(f"导入模型失败: {str(e)}")
+ return None
+
+ # def createSpotLight(self, pos=(0, 0, 0)):
+ # """创建聚光灯 - 使用统一的create_item方法"""
+ # try:
+ # # 调用CustomTreeWidget的create_item方法创建聚光灯节点
+ # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
+ # tree_widget = self.world.interface_manager.treeWidget
+ # if tree_widget and hasattr(tree_widget, 'create_item'):
+ # # 创建聚光灯节点
+ # created_nodes = tree_widget.create_item("spot_light")
+ #
+ # if created_nodes:
+ # # 获取创建的节点
+ # light_np, qt_item = created_nodes[0]
+ #
+ # # 设置位置(如果指定了非默认位置)
+ # if pos != (0, 0, 0):
+ # light_np.setPos(*pos)
+ # # 同时更新光源对象的位置
+ # light_obj = light_np.getPythonTag("rp_light_object")
+ # if light_obj:
+ # light_obj.setPos(*pos)
+ #
+ # print(f"✅ 通过create_item创建聚光灯成功: {light_np.getName()}")
+ # return light_np
+ # else:
+ # print("❌ create_item创建聚光灯失败")
+ # return None
+ # else:
+ # print("❌ 无法访问树形控件的create_item方法")
+ # return None
+ # else:
+ # print("❌ 无法访问界面管理器或树形控件")
+ # return None
+ #
+ # except Exception as e:
+ # print(f"❌ 创建聚光灯时发生错误: {str(e)}")
+ # import traceback
+ # traceback.print_exc()
+ # return None
+
+ # def createSpotLight(self, pos=(0, 0, 0)):
+ # from RenderPipelineFile.rpcore import SpotLight, RenderPipeline
+ # from panda3d.core import Vec3,NodePath
+ #
+ # render_pipeline = get_render_pipeline()
+ #
+ # # 创建一个挂载节点(你控制的)
+ # light_np = NodePath("SpotlightAttachNode")
+ # light_np.reparentTo(self.world.render)
+ # #light_np.setPos(*pos)
+ #
+ # self.half_energy = 5000
+ # self.lamp_fov = 70
+ # self.lamp_radius = 1000
+ #
+ # light = SpotLight()
+ # light.direction = Vec3(0, 0, -1) # 光照方向
+ # light.fov = self.lamp_fov # 光源角度(类似手电筒)
+ # light.set_color_from_temperature(5 * 1000.0) # 色温(K)
+ # light.energy = self.half_energy # 光照强度
+ # light.radius = self.lamp_radius # 影响范围
+ # light.casts_shadows = True # 是否投射阴影
+ # light.shadow_map_resolution = 256 # 阴影分辨率
+ # light.setPos(*pos)
+ # #light_np.setPos(*pos)
+ #
+ # #light_np = render_pipeline.add_light(light, parent=self.world.render)
+ # render_pipeline.add_light(light) # 添加到渲染管线
+ #
+ # light_name = f"Spotlight_{len(self.Spotlight)}"
+ #
+ # light_np.setName(light_name) # 设置唯一名称
+ # #light_np.reparentTo(self.world.render) # 挂载到场景根节点
+ #
+ # light_np.setTag("light_type", "spot_light")
+ # light_np.setTag("is_scene_element", "1")
+ # light_np.setTag("light_energy", str(light.energy))
+ #
+ # light_np.setPythonTag("rp_light_object", light)
+ #
+ # self.Spotlight.append(light_np)
+ #
+ # if hasattr(self.world, 'updateSceneTree'):
+ # self.world.updateSceneTree()
+ #
+ # #print("nikan"+light_np.getHpr())
+
+ # def createPointLight(self, pos=(0, 0, 0)):
+ # """创建点光源 - 使用统一的create_item方法"""
+ # try:
+ # # 调用CustomTreeWidget的create_item方法创建点光源节点
+ # if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
+ # tree_widget = self.world.interface_manager.treeWidget
+ # if tree_widget and hasattr(tree_widget, 'create_item'):
+ # # 创建点光源节点
+ # created_nodes = tree_widget.create_item("point_light")
+ #
+ # if created_nodes:
+ # # 获取创建的节点
+ # light_np, qt_item = created_nodes[0]
+ #
+ # # 设置位置(如果指定了非默认位置)
+ # if pos != (0, 0, 0):
+ # light_np.setPos(*pos)
+ # # 同时更新光源对象的位置
+ # light_obj = light_np.getPythonTag("rp_light_object")
+ # if light_obj:
+ # light_obj.setPos(*pos)
+ #
+ # print(f"✅ 通过create_item创建点光源成功: {light_np.getName()}")
+ # return light_np
+ # else:
+ # print("❌ create_item创建点光源失败")
+ # return None
+ # else:
+ # print("❌ 无法访问树形控件的create_item方法")
+ # return None
+ # else:
+ # print("❌ 无法访问界面管理器或树形控件")
+ # return None
+ #
+ # except Exception as e:
+ # print(f"❌ 创建点光源时发生错误: {str(e)}")
+ # import traceback
+ # traceback.print_exc()
+ # return None
+
+ # def createPointLight(self, pos=(0, 0, 0)):
+ # from RenderPipelineFile.rpcore import PointLight, RenderPipeline
+ # from panda3d.core import Vec3, NodePath
+ #
+ # render_pipeline = get_render_pipeline()
+ #
+ # # 创建一个挂载节点(你控制的)
+ # light_np = NodePath("PointlightAttachNode")
+ # light_np.reparentTo(self.world.render)
+ #
+ #
+ # light = PointLight()
+ # light.setPos(*pos)
+ # light_np.setPos(*pos)
+ # light.energy = 5000
+ # light.radius = 1000
+ # light.inner_radius = 0.4
+ # light.set_color_from_temperature(5 * 1000.0) # 色温(K)
+ # light.casts_shadows = True # 是否投射阴影
+ # light.shadow_map_resolution = 256 # 阴影分辨率
+ #
+ # render_pipeline.add_light(light) # 添加到渲染管线
+ #
+ # light_name = f"Pointlight{len(self.Pointlight)}"
+ #
+ # light_np.setName(light_name) # 设置唯一名称
+ #
+ # #light_np = NodePath(f"PointLight_{len(self.Pointlight)}")
+ # #light_np.reparentTo(self.world.render)
+ # #light_np.setPos(*pos)
+ #
+ # light_np.setTag("light_type", "point_light")
+ # light_np.setTag("is_scene_element", "1")
+ # light_np.setTag("light_energy", str(light.energy))
+ #
+ # # 保存光源对象引用(重要!用于属性面板)
+ # light_np.setPythonTag("rp_light_object", light)
+ #
+ # self.Pointlight.append(light_np)
+ #
+ # if hasattr(self.world, 'updateSceneTree'):
+ # self.world.updateSceneTree()
+ #
+ # return light,light_np
# ==================== GLB 转换方法 ====================
-
+
+ #===================================================
+
def _shouldConvertToGLB(self, filepath):
"""判断是否应该转换为GLB格式"""
ext = os.path.splitext(filepath)[1].lower()
diff --git a/ui/interface_manager.py b/ui/interface_manager.py
index b3fbd8f3..6a3aa771 100644
--- a/ui/interface_manager.py
+++ b/ui/interface_manager.py
@@ -18,8 +18,8 @@ class InterfaceManager:
self.treeWidget = treeWidget
# 添加右键菜单
- self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu)
- self.treeWidget.customContextMenuRequested.connect(self.showTreeContextMenu)
+ # self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu)
+ # self.treeWidget.customContextMenuRequested.connect(self.showTreeContextMenu)
# 更新场景树
self.world.scene_manager.updateSceneTree()
@@ -155,10 +155,12 @@ class InterfaceManager:
# 创建场景根节点
sceneRoot = QTreeWidgetItem(self.treeWidget, ['场景'])
-
+ sceneRoot.setData(0, Qt.UserRole, self.world.render)
+ sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT")
# 添加相机节点
cameraItem = QTreeWidgetItem(sceneRoot, ['相机'])
cameraItem.setData(0, Qt.UserRole, self.world.cam)
+ cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE")
print("添加相机节点")
# # 添加模型节点组
@@ -223,6 +225,7 @@ class InterfaceManager:
if hasattr(self.world, 'ground') and self.world.ground:
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
groundItem.setData(0, Qt.UserRole, self.world.ground)
+ groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
# 展开所有节点
#self.treeWidget.expandAll()
diff --git a/ui/main_window.py b/ui/main_window.py
index bd90fd3b..ed278dac 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -8,13 +8,15 @@
"""
import sys
+
+from PyQt5.QtGui import QKeySequence, QIcon
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea,
QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout,
- QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget)
-from PyQt5.QtCore import Qt, QDir, QTimer
-from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget
+ QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QFrame)
+from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint
+from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget, CustomAssetsTreeWidget, CustomConsoleDockWidget
class MainWindow(QMainWindow):
"""主窗口类"""
@@ -22,15 +24,17 @@ class MainWindow(QMainWindow):
def __init__(self, world):
super().__init__()
self.world = world
+ self.world.main_window = self # 关键:让world对象能访问主窗口
+
self.setupCenterWidget() # 创建中间部分Panda3D
self.setupMenus() # 创建菜单栏
self.setupDockWindows()
- self.setupToolbar()
+ # self.setupToolbar()
self.connectEvents()
# 移动窗口到屏幕中央
self.move_center()
-
+
# 创建定时器来更新脚本管理面板状态
self.updateTimer = QTimer()
self.updateTimer.timeout.connect(self.updateScriptPanel)
@@ -44,6 +48,9 @@ class MainWindow(QMainWindow):
self.pandaWidget = CustomPanda3DWidget(self.world)
self.setCentralWidget(self.pandaWidget)
+ # 创建内嵌工具栏
+ self.setupEmbeddedToolbar()
+
def move_center(self):
"""设置窗口居中显示"""
self.setGeometry(50, 50, 1920, 1080)
@@ -54,10 +61,208 @@ class MainWindow(QMainWindow):
int(screen.height() / 2 - self.height() / 2),
)
+ def setupEmbeddedToolbar(self):
+ """创建Unity风格的内嵌工具栏"""
+ # 创建工具栏容器
+ self.embeddedToolbar = QFrame(self.pandaWidget)
+ self.embeddedToolbar.setObjectName("UnityToolbar")
+ self.embeddedToolbar.setStyleSheet("""
+ QFrame#UnityToolbar {
+ background-color: rgba(240, 240, 240, 180);
+ border: 1px solid rgba(200, 200, 200, 200);
+ border-radius: 4px;
+ }
+ QToolButton {
+ background-color: rgba(250, 250, 250, 150);
+ border: none;
+ color: #333333;
+ padding: 5px;
+ border-radius: 3px;
+ }
+ QToolButton:hover {
+ background-color: rgba(220, 220, 220, 200);
+ }
+ QToolButton:checked {
+ background-color: rgba(100, 150, 220, 180);
+ color: white;
+ }
+ QToolButton:pressed {
+ background-color: rgba(80, 130, 200, 200);
+ }
+ """)
+
+ # 水平布局
+ self.toolbarLayout = QHBoxLayout(self.embeddedToolbar)
+ self.toolbarLayout.setContentsMargins(5, 5, 5, 5)
+ self.toolbarLayout.setSpacing(2)
+
+ # 创建工具按钮组
+ self.toolGroup = QButtonGroup()
+
+ # 选择工具
+ self.selectTool = QToolButton()
+ self.selectTool.setIcon(QIcon("icons/select_tool.png")) # 使用图标资源
+ self.selectTool.setText('选择')
+ self.selectTool.setIconSize(QSize(16, 16))
+ self.selectTool.setCheckable(True)
+ self.selectTool.setToolTip("选择工具 (Q)")
+ # self.selectTool.setShortcut(QKeySequence("Q"))
+ self.toolGroup.addButton(self.selectTool)
+ self.toolbarLayout.addWidget(self.selectTool)
+
+ # 移动工具
+ self.moveTool = QToolButton()
+ self.moveTool.setIcon(QIcon("icons/move_tool.png"))
+ self.moveTool.setText('移动')
+ self.moveTool.setIconSize(QSize(16, 16))
+ self.moveTool.setCheckable(True)
+ self.moveTool.setToolTip("移动工具 (W)")
+ # self.moveTool.setShortcut(QKeySequence("W"))
+ self.toolGroup.addButton(self.moveTool)
+ self.toolbarLayout.addWidget(self.moveTool)
+
+ # 旋转工具
+ self.rotateTool = QToolButton()
+ self.rotateTool.setIcon(QIcon("icons/rotate_tool.png"))
+ self.rotateTool.setText('旋转')
+ self.rotateTool.setIconSize(QSize(16, 16))
+ self.rotateTool.setCheckable(True)
+ self.rotateTool.setToolTip("旋转工具 (E)")
+ # self.rotateTool.setShortcut(QKeySequence("E"))
+ self.toolGroup.addButton(self.rotateTool)
+ self.toolbarLayout.addWidget(self.rotateTool)
+
+ # 缩放工具
+ self.scaleTool = QToolButton()
+ self.scaleTool.setIcon(QIcon("icons/scale_tool.png"))
+ self.scaleTool.setText('缩放')
+ self.scaleTool.setIconSize(QSize(16, 16))
+ self.scaleTool.setCheckable(True)
+ self.scaleTool.setToolTip("缩放工具 (R)")
+ # self.scaleTool.setShortcut(QKeySequence("R"))
+ self.toolGroup.addButton(self.scaleTool)
+ self.toolbarLayout.addWidget(self.scaleTool)
+
+ # 添加分隔符
+ separator = QFrame()
+ separator.setFrameShape(QFrame.VLine)
+ separator.setFrameShadow(QFrame.Sunken)
+ separator.setStyleSheet("background-color: rgba(240, 240, 240, 255);")
+ separator.setFixedWidth(1)
+ self.toolbarLayout.addWidget(separator)
+
+ # 设置位置到左上角
+ self.embeddedToolbar.move(10, 10)
+ self.embeddedToolbar.adjustSize()
+ self.embeddedToolbar.show()
+
+ # 连接事件
+ self.toolGroup.buttonClicked.connect(self.onToolChanged)
+ # 设置工具栏拖拽事件
+ self.embeddedToolbar.mousePressEvent = self.toolbarMousePressEvent
+ self.embeddedToolbar.mouseMoveEvent = self.toolbarMouseMoveEvent
+ self.embeddedToolbar.mouseReleaseEvent = self.toolbarMouseReleaseEvent
+
+ # 默认选择"选择"工具
+ self.selectTool.setChecked(True)
+ self.world.setCurrentTool("选择")
+
+ def toolbarMousePressEvent(self, event):
+ """工具栏鼠标按下事件"""
+ if event.button() == Qt.LeftButton:
+ self.toolbarDragging = True
+ self.dragStartPos = event.globalPos()
+ self.toolbarStartPos = self.embeddedToolbar.pos()
+ event.accept()
+
+ def toolbarMouseMoveEvent(self, event):
+ """工具栏鼠标移动事件"""
+ if self.toolbarDragging and event.buttons() == Qt.LeftButton:
+ # 计算新位置
+ delta = event.globalPos() - self.dragStartPos
+ new_pos = self.toolbarStartPos + delta
+
+ # 边界检测
+ panda_rect = self.pandaWidget.geometry()
+ toolbar_size = self.embeddedToolbar.size()
+
+ # 限制在Panda3D区域内
+ new_pos.setX(max(0, min(new_pos.x(), panda_rect.width() - toolbar_size.width())))
+ new_pos.setY(max(0, min(new_pos.y(), panda_rect.height() - toolbar_size.height())))
+
+ self.embeddedToolbar.move(new_pos)
+ event.accept()
+
+ def toolbarMouseReleaseEvent(self, event):
+ """工具栏鼠标释放事件"""
+ if event.button() == Qt.LeftButton and self.toolbarDragging:
+ self.toolbarDragging = False
+
+ # 自动吸附到最近的预设位置
+ self.snapToNearestPosition()
+ event.accept()
+
+ def snapToNearestPosition(self):
+ """自动吸附到最近的预设位置"""
+ current_pos = self.embeddedToolbar.pos()
+ panda_rect = self.pandaWidget.geometry()
+ toolbar_size = self.embeddedToolbar.size()
+
+ margin = 10
+
+ # 定义所有预设位置
+ positions = {
+ "top_center": QPoint(
+ (panda_rect.width() - toolbar_size.width()) // 2,
+ margin
+ ),
+ "top_left": QPoint(margin, margin),
+ "top_right": QPoint(
+ panda_rect.width() - toolbar_size.width() - margin,
+ margin
+ ),
+ "bottom_center": QPoint(
+ (panda_rect.width() - toolbar_size.width()) // 2,
+ panda_rect.height() - toolbar_size.height() - margin
+ ),
+ "bottom_left": QPoint(
+ margin,
+ panda_rect.height() - toolbar_size.height() - margin
+ ),
+ "bottom_right": QPoint(
+ panda_rect.width() - toolbar_size.width() - margin,
+ panda_rect.height() - toolbar_size.height() - margin
+ )
+ }
+
+ # 找到最近的位置
+ min_distance = float('inf')
+ nearest_position = "top_center"
+
+ for pos_name, pos_point in positions.items():
+ distance = ((current_pos.x() - pos_point.x()) ** 2 +
+ (current_pos.y() - pos_point.y()) ** 2) ** 0.5
+ if distance < min_distance:
+ min_distance = distance
+ nearest_position = pos_name
+
+ # 更新位置并平滑移动
+ self.toolbarPosition = nearest_position
+ target_pos = positions[nearest_position]
+
+ # 简单的平滑移动动画
+ from PyQt5.QtCore import QPropertyAnimation, QEasingCurve
+ self.toolbarAnimation = QPropertyAnimation(self.embeddedToolbar, b"pos")
+ self.toolbarAnimation.setDuration(200)
+ self.toolbarAnimation.setStartValue(current_pos)
+ self.toolbarAnimation.setEndValue(target_pos)
+ self.toolbarAnimation.setEasingCurve(QEasingCurve.OutCubic)
+ self.toolbarAnimation.start()
+
def setupMenus(self):
"""创建菜单栏"""
menubar = self.menuBar()
-
+
# 文件菜单
self.fileMenu = menubar.addMenu('文件')
self.newAction = self.fileMenu.addAction('新建')
@@ -66,7 +271,7 @@ class MainWindow(QMainWindow):
self.buildAction = self.fileMenu.addAction('打包')
self.fileMenu.addSeparator()
self.exitAction = self.fileMenu.addAction('退出')
-
+
# 编辑菜单
self.editMenu = menubar.addMenu('编辑')
self.undoAction = self.editMenu.addAction('撤销')
@@ -75,7 +280,7 @@ class MainWindow(QMainWindow):
self.cutAction = self.editMenu.addAction('剪切')
self.copyAction = self.editMenu.addAction('复制')
self.pasteAction = self.editMenu.addAction('粘贴')
-
+
# 视图菜单
self.viewMenu = menubar.addMenu('视图')
self.viewPerspectiveAction = self.viewMenu.addAction('透视图')
@@ -83,7 +288,7 @@ class MainWindow(QMainWindow):
self.viewFrontAction = self.viewMenu.addAction('前视图')
self.viewMenu.addSeparator()
self.viewGridAction = self.viewMenu.addAction('显示网格')
-
+
# 工具菜单
self.toolsMenu = menubar.addMenu('工具')
self.selectAction = self.toolsMenu.addAction('选择工具')
@@ -93,39 +298,19 @@ class MainWindow(QMainWindow):
self.sunsetAction = self.toolsMenu.addAction('光照编辑')
self.pluginAction = self.toolsMenu.addAction('图形编辑')
- # 创建菜单
+ # 统一创建菜单 - 关键修改
self.createMenu = menubar.addMenu('创建')
- self.createEnptyaddAction = self.createMenu.addAction('空对象')
- self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象')
+ self.setupCreateMenuActions() # 统一创建菜单动作
- self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
- self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
-
- self.createGUIaddMenu = self.createMenu.addMenu('GUI')
- self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
- self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
- self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
- self.createGUIaddMenu.addSeparator()
- self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
-
- self.createLightaddMenu = self.createMenu.addMenu('光源')
- self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
- self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
-
- # GUI菜单
+ # GUI菜单 - 复用创建菜单的动作
self.guiMenu = menubar.addMenu('GUI')
self.guiEditModeAction = self.guiMenu.addAction('进入GUI编辑模式')
self.guiMenu.addSeparator()
- # self.createButtonAction = self.guiMenu.addAction('创建按钮')
- # self.createLabelAction = self.guiMenu.addAction('创建标签')
- # self.createEntryAction = self.guiMenu.addAction('创建输入框')
self.guiMenu.addAction(self.createButtonAction)
self.guiMenu.addAction(self.createLabelAction)
self.guiMenu.addAction(self.createEntryAction)
self.guiMenu.addSeparator()
- # self.create3DTextAction = self.guiMenu.addAction('创建3D文本')
self.guiMenu.addAction(self.create3DTextAction)
- # self.createVirtualScreenAction = self.guiMenu.addAction('创建虚拟屏幕')
self.guiMenu.addAction(self.createVirtualScreenAction)
# 脚本菜单
@@ -139,37 +324,100 @@ class MainWindow(QMainWindow):
self.toggleHotReloadAction.setChecked(True) # 默认启用
self.scriptMenu.addSeparator()
self.openScriptsManagerAction = self.scriptMenu.addAction('脚本管理器')
-
+
# 帮助菜单
self.helpMenu = menubar.addMenu('帮助')
self.aboutAction = self.helpMenu.addAction('关于')
-
+
+ def setupCreateMenuActions(self):
+ """统一设置创建菜单的所有动作 - 避免重复代码"""
+ # 基础对象
+ self.createEnptyaddAction = self.createMenu.addAction('空对象')
+
+ # 3D对象子菜单
+ self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象')
+ # 可以在这里添加更多3D对象类型
+
+ # 3D GUI子菜单
+ self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
+ self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
+
+ # GUI子菜单
+ self.createGUIaddMenu = self.createMenu.addMenu('GUI')
+ self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
+ self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
+ self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
+ self.createGUIaddMenu.addSeparator()
+ self.createVideoScreen = self.createGUIaddMenu.addAction('创建视频屏幕')
+ self.createSphericalVideo = self.createGUIaddMenu.addAction('创建球形视频')
+ self.createGUIaddMenu.addSeparator()
+ self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
+
+ # 光源子菜单
+ self.createLightaddMenu = self.createMenu.addMenu('光源')
+ self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
+ self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
+
+ # 统一连接信号到处理方法
+ self.connectCreateMenuActions()
+
+ def connectCreateMenuActions(self):
+ """统一连接创建菜单的信号到处理方法"""
+ # 连接到world对象的创建方法
+ # self.createEnptyaddAction.triggered.connect(self.world.createEmptyObject)
+ self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
+ self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
+ self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
+ self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
+ # self.createVideoScreen.triggered.connect(self.world.createVideoScreen)
+ # self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo)
+ # self.createVirtualScreenAction.triggered.connect(self.world.createVirtualScreen)
+ self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
+ self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
+
+ # # self.createVideoScreen.triggered.connect(lambda: self.world.video_manager.create_video_screen(
+ # # pos=(0, 0, 2),
+ # # size=(4, 3),
+ # # name=f"video_screen_{len(self.world.video_manager.video_objects) if hasattr(self.world, 'video_manager') else 0}"
+ # # ))
+ # # self.createSphericalVideo.triggered.connect(lambda: self.world.createGUISphericalVideo())
+ # self.createVirtualScreenAction.triggered.connect(lambda: self.world.create_spherical_video())
+
+ def getCreateMenuActions(self):
+ """获取所有创建菜单动作的字典 - 供右键菜单使用"""
+ return {
+ 'createEmpty': self.createEnptyaddAction,
+ 'create3DText': self.create3DTextAction,
+ 'createButton': self.createButtonAction,
+ 'createLabel': self.createLabelAction,
+ 'createEntry': self.createEntryAction,
+ 'createVideoScreen': self.createVideoScreen,
+ 'createSphericalVideo': self.createSphericalVideo,
+ 'createVirtualScreen': self.createVirtualScreenAction,
+ 'createSpotLight': self.createSpotLightAction,
+ 'createPointLight': self.createPointLightAction,
+ }
+
def setupDockWindows(self):
"""创建停靠窗口"""
# 创建左侧停靠窗口(层级窗口)
self.leftDock = QDockWidget("层级", self)
- self.leftDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
self.treeWidget = CustomTreeWidget(self.world)
self.world.setTreeWidget(self.treeWidget) # 设置树形控件引用
self.leftDock.setWidget(self.treeWidget)
- # self.leftDock.setMinimumWidth(300)
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock)
-
# 创建右侧停靠窗口(属性窗口)
self.rightDock = QDockWidget("属性", self)
- self.rightDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
# 创建属性面板的主容器和布局
self.propertyContainer = QWidget()
self.propertyContainer.setObjectName("PropertyContainer")
self.propertyLayout = QVBoxLayout(self.propertyContainer)
- # self.propertyLayout = QFormLayout(self.propertyContainer)
# 添加初始提示信息
tipLabel = QLabel("")
tipLabel.setStyleSheet("color: gray;") # 使用灰色字体
- # self.propertyLayout.addRow(tipLabel)
self.propertyLayout.addWidget(tipLabel)
# 创建滚动区域并设置属性
@@ -185,81 +433,109 @@ class MainWindow(QMainWindow):
# 设置属性面板到世界对象
self.world.setPropertyLayout(self.propertyLayout)
-
# 创建脚本管理停靠窗口
self.scriptDock = QDockWidget("脚本管理", self)
- self.scriptDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
- self.setupScriptPanel()
+
+ # 创建脚本面板的主容器和布局(与属性面板相同结构)
+ self.scriptContainer = QWidget()
+ self.scriptContainer.setObjectName("ScriptContainer")
+ self.scriptLayout = QVBoxLayout(self.scriptContainer)
+
+ # 创建滚动区域并设置属性
+ self.scriptScrollArea = QScrollArea()
+ self.scriptScrollArea.setWidgetResizable(True)
+ self.scriptScrollArea.setWidget(self.scriptContainer)
+
+ # 设置滚动区域为停靠窗口的主部件
+ self.scriptDock.setWidget(self.scriptScrollArea)
+ self.scriptDock.setMinimumWidth(300)
+
+ # 设置脚本面板内容
+ self.setupScriptPanel(self.scriptLayout)
self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock)
-
+
# 将右侧停靠窗口设为标签形式
self.tabifyDockWidget(self.rightDock, self.scriptDock)
-
+
+ # # 创建底部停靠窗口(资源窗口)
+ # self.bottomDock = QDockWidget("资源", self)
+ # self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea)
+ #
+ # # 创建文件系统模型
+ # self.fileModel = QFileSystemModel()
+ # self.fileModel.setRootPath(QDir.homePath()) # 设置为用户主目录
+ #
+ # # 创建树形视图显示文件系统
+ # self.fileView = CustomFileView(self.world)
+ # self.fileView.setModel(self.fileModel)
+ # self.fileView.setRootIndex(self.fileModel.index(QDir.homePath())) # 设置为用户主目录索引
+ #
+ # # 设置列宽
+ # self.fileView.setColumnWidth(0, 250) # 名称列
+ # self.fileView.setColumnWidth(1, 100) # 大小列
+ # self.fileView.setColumnWidth(2, 100) # 类型列
+ # self.fileView.setColumnWidth(3, 150) # 修改日期列
+ #
+ # # 设置视图属性
+ # self.fileView.setMinimumHeight(200) # 设置最小高度
+ #
+ # self.bottomDock.setWidget(self.fileView)
+ # self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
+
+
# 创建底部停靠窗口(资源窗口)
self.bottomDock = QDockWidget("资源", self)
- self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea)
-
- # 创建文件系统模型
- self.fileModel = QFileSystemModel()
- self.fileModel.setRootPath(QDir.homePath()) # 设置为用户主目录
-
- # 创建树形视图显示文件系统
- self.fileView = CustomFileView(self.world)
- self.fileView.setModel(self.fileModel)
- self.fileView.setRootIndex(self.fileModel.index(QDir.homePath())) # 设置为用户主目录索引
-
- # 设置列宽
- self.fileView.setColumnWidth(0, 250) # 名称列
- self.fileView.setColumnWidth(1, 100) # 大小列
- self.fileView.setColumnWidth(2, 100) # 类型列
- self.fileView.setColumnWidth(3, 150) # 修改日期列
-
- # 设置视图属性
- self.fileView.setMinimumHeight(200) # 设置最小高度
-
+ self.fileView = CustomAssetsTreeWidget(self.world)
self.bottomDock.setWidget(self.fileView)
self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
-
+
+ # 创建底部停靠控制台
+ self.consoleDock = QDockWidget("控制台", self)
+ self.consoleView = CustomConsoleDockWidget(self.world)
+ self.consoleDock.setWidget(self.consoleView)
+ self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
+
+
def setupToolbar(self):
"""创建工具栏"""
self.toolbar = self.addToolBar('工具栏')
-
+
# 创建工具按钮组
self.toolGroup = QButtonGroup()
-
+
# 选择工具
self.selectTool = QToolButton()
self.selectTool.setText("选择")
self.selectTool.setCheckable(True)
self.toolGroup.addButton(self.selectTool)
self.toolbar.addWidget(self.selectTool)
-
+
# 旋转工具
self.rotateTool = QToolButton()
self.rotateTool.setText("旋转")
self.rotateTool.setCheckable(True)
self.toolGroup.addButton(self.rotateTool)
self.toolbar.addWidget(self.rotateTool)
-
+
# 缩放工具
self.scaleTool = QToolButton()
self.scaleTool.setText("缩放")
self.scaleTool.setCheckable(True)
self.toolGroup.addButton(self.scaleTool)
self.toolbar.addWidget(self.scaleTool)
-
+
# 添加分隔符
self.toolbar.addSeparator()
-
+
# GUI创建工具
self.createButtonTool = QToolButton()
self.createButtonTool.setText("创建按钮")
self.toolbar.addWidget(self.createButtonTool)
-
+
self.createLabelTool = QToolButton()
self.createLabelTool.setText("创建标签")
self.toolbar.addWidget(self.createLabelTool)
-
+
self.create3DTextTool = QToolButton()
self.create3DTextTool.setText("3D文本")
self.toolbar.addWidget(self.create3DTextTool)
@@ -275,32 +551,28 @@ class MainWindow(QMainWindow):
# 默认选择"选择"工具
self.selectTool.setChecked(True)
self.world.setCurrentTool("选择")
-
- def setupScriptPanel(self):
+
+ def setupScriptPanel(self, layout):
"""创建脚本管理面板"""
- # 创建主容器
- scriptContainer = QWidget()
- layout = QVBoxLayout(scriptContainer)
-
# 脚本状态组
statusGroup = QGroupBox("脚本系统状态")
statusLayout = QVBoxLayout()
-
+
self.scriptStatusLabel = QLabel("脚本系统: 已启动")
self.scriptStatusLabel.setStyleSheet("color: green; font-weight: bold;")
statusLayout.addWidget(self.scriptStatusLabel)
-
+
self.hotReloadLabel = QLabel("热重载: 已启用")
self.hotReloadLabel.setStyleSheet("color: blue;")
statusLayout.addWidget(self.hotReloadLabel)
-
+
statusGroup.setLayout(statusLayout)
layout.addWidget(statusGroup)
-
+
# 脚本创建组
createGroup = QGroupBox("创建脚本")
createLayout = QVBoxLayout()
-
+
# 脚本名称输入
nameLayout = QHBoxLayout()
nameLayout.addWidget(QLabel("脚本名称:"))
@@ -308,7 +580,7 @@ class MainWindow(QMainWindow):
self.scriptNameEdit.setPlaceholderText("输入脚本名称...")
nameLayout.addWidget(self.scriptNameEdit)
createLayout.addLayout(nameLayout)
-
+
# 模板选择
templateLayout = QHBoxLayout()
templateLayout.addWidget(QLabel("模板:"))
@@ -316,90 +588,87 @@ class MainWindow(QMainWindow):
self.templateCombo.addItems(["basic", "movement", "animation"])
templateLayout.addWidget(self.templateCombo)
createLayout.addLayout(templateLayout)
-
+
# 创建按钮
self.createScriptBtn = QPushButton("创建脚本")
self.createScriptBtn.clicked.connect(self.onCreateScript)
createLayout.addWidget(self.createScriptBtn)
-
+
createGroup.setLayout(createLayout)
layout.addWidget(createGroup)
-
+
# 可用脚本组
scriptsGroup = QGroupBox("可用脚本")
scriptsLayout = QVBoxLayout()
-
+
# 脚本列表
self.scriptsList = QListWidget()
self.scriptsList.itemDoubleClicked.connect(self.onScriptDoubleClick)
scriptsLayout.addWidget(self.scriptsList)
-
+
# 脚本操作按钮
scriptButtonsLayout = QHBoxLayout()
-
+
self.loadScriptBtn = QPushButton("加载脚本")
self.loadScriptBtn.clicked.connect(self.onLoadScript)
scriptButtonsLayout.addWidget(self.loadScriptBtn)
-
+
self.reloadAllBtn = QPushButton("重载全部")
self.reloadAllBtn.clicked.connect(self.onReloadAllScripts)
scriptButtonsLayout.addWidget(self.reloadAllBtn)
-
+
scriptsLayout.addLayout(scriptButtonsLayout)
scriptsGroup.setLayout(scriptsLayout)
layout.addWidget(scriptsGroup)
-
+
# 脚本挂载组
mountGroup = QGroupBox("脚本挂载")
mountLayout = QVBoxLayout()
-
+
# 当前选中对象显示
self.selectedObjectLabel = QLabel("未选择对象")
self.selectedObjectLabel.setStyleSheet("color: gray; font-style: italic;")
mountLayout.addWidget(self.selectedObjectLabel)
-
+
# 脚本选择和挂载
mountControlLayout = QHBoxLayout()
-
+
self.mountScriptCombo = QComboBox()
self.mountScriptCombo.setEnabled(False)
mountControlLayout.addWidget(self.mountScriptCombo)
-
+
self.mountBtn = QPushButton("挂载")
self.mountBtn.setEnabled(False)
self.mountBtn.clicked.connect(self.onMountScript)
mountControlLayout.addWidget(self.mountBtn)
-
+
mountLayout.addLayout(mountControlLayout)
-
+
# 已挂载脚本列表
self.mountedScriptsList = QListWidget()
self.mountedScriptsList.setMaximumHeight(100)
mountLayout.addWidget(QLabel("已挂载脚本:"))
mountLayout.addWidget(self.mountedScriptsList)
-
+
# 卸载按钮
self.unmountBtn = QPushButton("卸载选中脚本")
self.unmountBtn.clicked.connect(self.onUnmountScript)
mountLayout.addWidget(self.unmountBtn)
-
+
mountGroup.setLayout(mountLayout)
layout.addWidget(mountGroup)
-
+
# 添加拉伸以填充剩余空间
layout.addStretch()
-
- # 设置到停靠窗口
- self.scriptDock.setWidget(scriptContainer)
-
+
# 初始化脚本列表
self.refreshScriptsList()
-
+
def connectEvents(self):
"""连接事件信号"""
# 导入项目管理功能函数
from main import createNewProject, saveProject, openProject, buildPackage
-
+
# 连接文件菜单事件
self.newAction.triggered.connect(lambda: createNewProject(self))
self.openAction.triggered.connect(lambda: openProject(self))
@@ -414,40 +683,48 @@ class MainWindow(QMainWindow):
# 连接GUI编辑模式事件
self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode())
- # 连接创建事件
- # 连接光源创建按钮事件
- self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
- self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
- # 连接GUI创建按钮事件
- self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
- self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
- self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
- self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
- #self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
- self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
-
- # 连接工具栏GUI创建按钮事件
- self.createButtonTool.clicked.connect(lambda: self.world.createGUIButton())
- self.createLabelTool.clicked.connect(lambda: self.world.createGUILabel())
- self.create3DTextTool.clicked.connect(lambda: self.world.createGUI3DText())
- self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight())
- self.createPointLight.clicked.connect(lambda :self.world.createPointLight())
-
+ # # 连接创建事件
+ # # 连接光源创建按钮事件
+ # self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
+ # self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
+ # # 连接GUI创建按钮事件
+ # self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
+ # self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
+ # self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
+ # self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
+ # #self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
+ #
+ # # self.createVideoScreen.triggered.connect(lambda: self.world.video_manager.create_video_screen(
+ # # pos=(0, 0, 2),
+ # # size=(4, 3),
+ # # name=f"video_screen_{len(self.world.video_manager.video_objects) if hasattr(self.world, 'video_manager') else 0}"
+ # # ))
+ # # self.createSphericalVideo.triggered.connect(lambda: self.world.createGUISphericalVideo())
+ #
+ # self.createVirtualScreenAction.triggered.connect(lambda: self.world.create_spherical_video())
+ #
+ # # 连接工具栏GUI创建按钮事件
+ # self.createButtonTool.clicked.connect(lambda: self.world.createGUIButton())
+ # self.createLabelTool.clicked.connect(lambda: self.world.createGUILabel())
+ # self.create3DTextTool.clicked.connect(lambda: self.world.createGUI3DText())
+ # self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight())
+ # self.createPointLight.clicked.connect(lambda :self.world.createPointLight())
+
# 连接树节点点击信号
# self.treeWidget.itemClicked.connect(self.world.onTreeItemClicked)
self.treeWidget.itemSelectionChanged.connect(lambda :self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0))
print("已连接点击信号")
-
+
# 连接工具切换信号
self.toolGroup.buttonClicked.connect(self.onToolChanged)
-
+
# 连接脚本菜单事件
self.createScriptAction.triggered.connect(self.onCreateScriptDialog)
self.loadScriptAction.triggered.connect(self.onLoadScriptFile)
self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts)
self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload)
self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager)
-
+
def onToolChanged(self, button):
"""工具切换事件处理"""
if button.isChecked():
@@ -457,29 +734,29 @@ class MainWindow(QMainWindow):
else:
self.world.setCurrentTool(None)
print("工具栏: 取消选择工具")
-
+
# ==================== 脚本管理事件处理 ====================
-
+
def refreshScriptsList(self):
"""刷新脚本列表"""
self.scriptsList.clear()
self.mountScriptCombo.clear()
-
+
available_scripts = self.world.getAvailableScripts()
for script_name in available_scripts:
self.scriptsList.addItem(script_name)
self.mountScriptCombo.addItem(script_name)
-
+
def updateScriptPanel(self):
"""更新脚本面板状态"""
# 更新热重载状态
hot_reload_enabled = self.world.script_manager.hot_reload_enabled
self.hotReloadLabel.setText(f"热重载: {'已启用' if hot_reload_enabled else '已禁用'}")
self.hotReloadLabel.setStyleSheet(f"color: {'blue' if hot_reload_enabled else 'gray'};")
-
+
# 更新热重载菜单状态
self.toggleHotReloadAction.setChecked(hot_reload_enabled)
-
+
# 更新选中对象信息
selected_object = getattr(self.world.selection, 'selectedObject', None)
if selected_object:
@@ -487,7 +764,7 @@ class MainWindow(QMainWindow):
self.selectedObjectLabel.setStyleSheet("color: green; font-weight: bold;")
self.mountScriptCombo.setEnabled(True)
self.mountBtn.setEnabled(True)
-
+
# 更新已挂载脚本列表
self.updateMountedScriptsList(selected_object)
else:
@@ -496,7 +773,7 @@ class MainWindow(QMainWindow):
self.mountScriptCombo.setEnabled(False)
self.mountBtn.setEnabled(False)
self.mountedScriptsList.clear()
-
+
def updateMountedScriptsList(self, game_object):
"""更新已挂载脚本列表"""
# 保存当前选中项的脚本名(去除状态前缀)
@@ -505,7 +782,7 @@ class MainWindow(QMainWindow):
if current_item:
# 提取脚本名(移除 "✓ " 或 "✗ " 前缀)
selected_script_name = current_item.text()[2:]
-
+
# 清空并重新填充列表
self.mountedScriptsList.clear()
scripts = self.world.getScripts(game_object)
@@ -514,7 +791,7 @@ class MainWindow(QMainWindow):
enabled = "✓" if script_component.enabled else "✗"
item_text = f"{enabled} {script_name}"
self.mountedScriptsList.addItem(item_text)
-
+
# 恢复选中状态(根据脚本名匹配)
if selected_script_name:
for i in range(self.mountedScriptsList.count()):
@@ -524,16 +801,16 @@ class MainWindow(QMainWindow):
if current_script_name == selected_script_name:
self.mountedScriptsList.setCurrentItem(item)
break
-
+
def onCreateScript(self):
"""创建脚本按钮事件"""
script_name = self.scriptNameEdit.text().strip()
if not script_name:
QMessageBox.warning(self, "错误", "请输入脚本名称!")
return
-
+
template = self.templateCombo.currentText()
-
+
try:
success = self.world.createScript(script_name, template)
if success:
@@ -544,7 +821,7 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(e)}")
-
+
def onCreateScriptDialog(self):
"""菜单创建脚本事件"""
script_name, ok = QInputDialog.getText(self, "创建脚本", "输入脚本名称:")
@@ -558,14 +835,14 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(e)}")
-
+
def onLoadScript(self):
"""加载脚本按钮事件"""
current_item = self.scriptsList.currentItem()
if not current_item:
QMessageBox.warning(self, "错误", "请选择要加载的脚本!")
return
-
+
script_name = current_item.text()
try:
success = self.world.reloadScript(script_name)
@@ -575,7 +852,7 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 重载失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"重载脚本时出错: {str(e)}")
-
+
def onLoadScriptFile(self):
"""加载脚本文件菜单事件"""
file_path, _ = QFileDialog.getOpenFileName(
@@ -591,7 +868,7 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "错误", "脚本文件加载失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载脚本文件时出错: {str(e)}")
-
+
def onReloadAllScripts(self):
"""重载所有脚本事件"""
try:
@@ -600,44 +877,44 @@ class MainWindow(QMainWindow):
self.refreshScriptsList()
except Exception as e:
QMessageBox.critical(self, "错误", f"重载脚本时出错: {str(e)}")
-
+
def onToggleHotReload(self):
"""切换热重载状态"""
enabled = self.toggleHotReloadAction.isChecked()
self.world.enableHotReload(enabled)
status = "启用" if enabled else "禁用"
QMessageBox.information(self, "热重载", f"热重载已{status}")
-
+
def onOpenScriptsManager(self):
"""打开脚本管理器"""
# 显示脚本管理停靠窗口
self.scriptDock.show()
self.scriptDock.raise_()
-
+
def onScriptDoubleClick(self, item):
"""脚本列表双击事件"""
# 可以在这里添加打开外部编辑器的功能
script_name = item.text()
QMessageBox.information(self, "提示", f"双击了脚本: {script_name}\n\n可以使用外部编辑器编辑脚本文件。")
-
+
def onMountScript(self):
"""挂载脚本事件"""
selected_object = getattr(self.world.selection, 'selectedObject', None)
if not selected_object:
QMessageBox.warning(self, "错误", "请先选择一个对象!")
return
-
+
script_name = self.mountScriptCombo.currentText()
if not script_name:
QMessageBox.warning(self, "错误", "请选择要挂载的脚本!")
return
-
+
try:
success = self.world.addScript(selected_object, script_name)
if success:
QMessageBox.information(self, "成功", f"脚本 '{script_name}' 已挂载到对象!")
self.updateMountedScriptsList(selected_object)
-
+
# 同时更新属性面板
if self.treeWidget and self.treeWidget.currentItem():
self.world.updatePropertyPanel(self.treeWidget.currentItem())
@@ -645,29 +922,29 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "错误", f"挂载脚本失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"挂载脚本时出错: {str(e)}")
-
+
def onUnmountScript(self):
"""卸载脚本事件"""
selected_object = getattr(self.world.selection, 'selectedObject', None)
if not selected_object:
QMessageBox.warning(self, "错误", "请先选择一个对象!")
return
-
+
current_item = self.mountedScriptsList.currentItem()
if not current_item:
QMessageBox.warning(self, "错误", "请选择要卸载的脚本!")
return
-
+
# 解析脚本名称(移除状态标记)
item_text = current_item.text()
script_name = item_text[2:] # 移除 "✓ " 或 "✗ " 前缀
-
+
try:
success = self.world.removeScript(selected_object, script_name)
if success:
QMessageBox.information(self, "成功", f"脚本 '{script_name}' 已从对象卸载!")
self.updateMountedScriptsList(selected_object)
-
+
# 同时更新属性面板
if self.treeWidget and self.treeWidget.currentItem():
self.world.updatePropertyPanel(self.treeWidget.currentItem())
@@ -713,4 +990,4 @@ def setup_main_window(world):
main_window = MainWindow(world)
main_window.show()
- return app, main_window
\ No newline at end of file
+ return app, main_window
\ No newline at end of file
diff --git a/ui/property_panel.py b/ui/property_panel.py
index d2c51790..137f6238 100644
--- a/ui/property_panel.py
+++ b/ui/property_panel.py
@@ -504,6 +504,7 @@ class PropertyPanelManager:
success = self.world.gui_manager.editGUIElement(gui_element, "text", text)
if success:
# 更新场景树显示的名称
+ pass #CH
self.world.scene_manager.updateSceneTree()
textEdit.textChanged.connect(updateText)
diff --git a/ui/widgets.py b/ui/widgets.py
index 52f97d1d..207f26d9 100644
--- a/ui/widgets.py
+++ b/ui/widgets.py
@@ -13,10 +13,12 @@ import re
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout,
QLineEdit, QPushButton, QLabel, QDialogButtonBox,
QTreeView, QTreeWidget, QTreeWidgetItem, QWidget,
- QFileDialog, QMessageBox, QAbstractItemView)
-from PyQt5.QtCore import Qt, QUrl
+ QFileDialog, QMessageBox, QAbstractItemView, QMenu, QDockWidget, QButtonGroup, QToolButton)
+from PyQt5.QtCore import Qt, QUrl, QMimeData
from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush
from PyQt5.sip import wrapinstance
+from panda3d.core import ModelRoot
+
from QPanda3D.QPanda3DWidget import QPanda3DWidget
@@ -103,10 +105,9 @@ class NewProjectDialog(QDialog):
self.accept()
-
class CustomPanda3DWidget(QPanda3DWidget):
"""自定义Panda3D显示部件"""
-
+
def __init__(self, world, parent=None):
if parent is None:
parent = wrapinstance(0, QWidget)
@@ -119,7 +120,7 @@ class CustomPanda3DWidget(QPanda3DWidget):
# 让world引用这个widget,以便获取准确的尺寸
if hasattr(world, 'setQtWidget'):
world.setQtWidget(self)
-
+
def getActualSize(self):
"""获取Qt部件的实际渲染尺寸"""
return (self.width(), self.height())
@@ -233,7 +234,6 @@ class CustomPanda3DWidget(QPanda3DWidget):
except Exception as e:
print(f"⚠️ 清理CustomPanda3DWidget资源时出错: {e}")
-
class CustomFileView(QTreeView):
"""自定义文件浏览器"""
@@ -242,40 +242,54 @@ class CustomFileView(QTreeView):
parent = wrapinstance(0, QWidget)
super().__init__(parent)
self.world = world
- self.setDragEnabled(True) # 启用拖拽
- self.setSelectionMode(QTreeView.ExtendedSelection) # 允许多选
+ self.setupUI()
+ self.setupDragDrop()
+
+ def setupUI(self):
+ """初始化UI设置"""
+ self.setHeaderHidden(True)
+ # 启用多选和拖拽
+ self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
+ self.setDropIndicatorShown(True) # 启用拖放指示线
+
+ def setupDragDrop(self):
+ """设置拖拽功能"""
+ # 使用自定义拖拽模式
self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入
-
+ self.setDefaultDropAction(Qt.DropAction.MoveAction)
+ self.setDragEnabled(True)
+ self.setAcceptDrops(True)
+
def startDrag(self, supportedActions):
"""开始拖拽操作"""
# 获取选中的文件
indexes = self.selectedIndexes()
if not indexes:
return
-
+
# 只处理文件名列
indexes = [idx for idx in indexes if idx.column() == 0]
-
+
# 创建 MIME 数据
mimeData = self.model().mimeData(indexes)
-
+
# 检查是否包含支持的模型文件
urls = []
for index in indexes:
filepath = self.model().filePath(index)
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
urls.append(QUrl.fromLocalFile(filepath))
-
+
if not urls:
return
-
+
# 设置 URL 列表
mimeData.setUrls(urls)
-
+
# 创建拖拽对象
drag = QDrag(self)
drag.setMimeData(mimeData)
-
+
# 设置拖拽图标(可选)
pixmap = QPixmap(32, 32)
pixmap.fill(Qt.transparent)
@@ -283,7 +297,7 @@ class CustomFileView(QTreeView):
painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(urls)))
painter.end()
drag.setPixmap(pixmap)
-
+
# 执行拖拽
drag.exec_(supportedActions)
@@ -293,7 +307,7 @@ class CustomFileView(QTreeView):
if index.isValid():
model = self.model()
filepath = model.filePath(index)
-
+
# 检查是否是模型文件
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
self.world.importModel(filepath)
@@ -301,16 +315,995 @@ class CustomFileView(QTreeView):
print("不支持的文件类型")
super().mouseDoubleClickEvent(event)
+class CustomAssetsTreeWidget(QTreeWidget):
+ def __init__(self, world, parent=None):
+ if parent is None:
+ parent = wrapinstance(0, QWidget)
+ super().__init__(parent)
+ self.world = world
+ self.root_path = None
+ self.setupUI()
+ self.setupDragDrop()
+
+ # 默认加载项目根路径
+ self.load_file_tree()
+ # 设置右键菜单
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+ self.customContextMenuRequested.connect(self.showContextMenu)
+
+ def showContextMenu(self, position):
+ """显示右键菜单"""
+ item = self.itemAt(position)
+ if not item:
+ return
+
+ filepath = item.data(0, Qt.UserRole)
+ is_folder = item.data(0, Qt.UserRole + 1)
+
+ menu = QMenu(self)
+
+ if is_folder:
+ # 文件夹右键菜单
+ menu.addAction("📁 新建文件夹", lambda: self.createNewFolder(item))
+ menu.addAction("📄 新建文件", lambda: self.createNewFile(item))
+ menu.addSeparator()
+ menu.addAction("✏️ 重命名", lambda: self.renameItem(item))
+ menu.addAction("🗑️ 删除", lambda: self.deleteItem(item))
+ menu.addSeparator()
+ menu.addAction("📋 复制路径", lambda: self.copyPath(filepath))
+ menu.addAction("🔍 查看属性", lambda: self.showProperties(item))
+ else:
+ # 文件右键菜单
+ menu.addAction("📂 打开文件", lambda: self.openFile(filepath))
+ menu.addSeparator()
+ menu.addAction("✏️ 重命名", lambda: self.renameItem(item))
+ menu.addAction("🗑️ 删除", lambda: self.deleteItem(item))
+ menu.addSeparator()
+ menu.addAction("📋 复制路径", lambda: self.copyPath(filepath))
+ menu.addAction("🔍 查看属性", lambda: self.showProperties(item))
+
+ # 显示菜单
+ menu.exec_(self.mapToGlobal(position))
+
+ def _saveExpandedState(self):
+ """保存展开状态"""
+ expanded_paths = set()
+
+ def collectExpanded(item):
+ if item.isExpanded():
+ path = item.data(0, Qt.UserRole)
+ if path:
+ expanded_paths.add(path)
+
+ for i in range(item.childCount()):
+ collectExpanded(item.child(i))
+
+ # 遍历所有顶级项目
+ for i in range(self.topLevelItemCount()):
+ collectExpanded(self.topLevelItem(i))
+
+ return expanded_paths
+
+ def _restoreExpandedState(self, expanded_paths):
+ """恢复展开状态"""
+ def restoreExpanded(item):
+ path = item.data(0, Qt.UserRole)
+ if path in expanded_paths:
+ item.setExpanded(True)
+
+ for i in range(item.childCount()):
+ restoreExpanded(item.child(i))
+
+ # 遍历所有顶级项目
+ for i in range(self.topLevelItemCount()):
+ restoreExpanded(self.topLevelItem(i))
+
+ def _refreshWithStatePreservation(self):
+ """刷新树形视图并保持状态"""
+ # 保存当前状态
+ expanded_paths = self._saveExpandedState()
+
+ # 刷新树形结构
+ self.load_file_tree()
+
+ # 恢复展开状态
+ self._restoreExpandedState(expanded_paths)
+
+ def createNewFolder(self, parent_item):
+ """创建新文件夹"""
+ import os
+ from PyQt5.QtWidgets import QInputDialog
+
+ parent_path = parent_item.data(0, Qt.UserRole)
+ folder_name, ok = QInputDialog.getText(self, "新建文件夹", "文件夹名称:")
+
+ if ok and folder_name:
+ new_folder_path = os.path.join(parent_path, folder_name)
+ try:
+ os.makedirs(new_folder_path, exist_ok=True)
+ self._refreshWithStatePreservation()
+ print(f"创建文件夹: {new_folder_path}")
+ except OSError as e:
+ print(f"创建文件夹失败: {e}")
+
+ def createNewFile(self, parent_item):
+ """创建新文件"""
+ import os
+ from PyQt5.QtWidgets import QInputDialog
+
+ parent_path = parent_item.data(0, Qt.UserRole)
+ file_name, ok = QInputDialog.getText(self, "新建文件", "文件名称:")
+
+ if ok and file_name:
+ new_file_path = os.path.join(parent_path, file_name)
+ try:
+ with open(new_file_path, 'w', encoding='utf-8') as f:
+ f.write("")
+ self._refreshWithStatePreservation()
+ print(f"创建文件: {new_file_path}")
+ except OSError as e:
+ print(f"创建文件失败: {e}")
+
+ def renameItem(self, item):
+ """重命名文件或文件夹"""
+ import os
+ from PyQt5.QtWidgets import QInputDialog
+
+ old_path = item.data(0, Qt.UserRole)
+ old_name = os.path.basename(old_path)
+
+ new_name, ok = QInputDialog.getText(self, "重命名", "新名称:", text=old_name)
+
+ if ok and new_name and new_name != old_name:
+ parent_dir = os.path.dirname(old_path)
+ new_path = os.path.join(parent_dir, new_name)
+
+ try:
+ os.rename(old_path, new_path)
+ self._refreshWithStatePreservation()
+ print(f"重命名: {old_path} -> {new_path}")
+ except OSError as e:
+ print(f"重命名失败: {e}")
+
+ def deleteItem(self, item):
+ """删除文件或文件夹"""
+ import os
+ import shutil
+ from PyQt5.QtWidgets import QMessageBox
+
+ filepath = item.data(0, Qt.UserRole)
+ is_folder = item.data(0, Qt.UserRole + 1)
+
+ item_type = "文件夹" if is_folder else "文件"
+ reply = QMessageBox.question(
+ self,
+ "确认删除",
+ f"确定要删除这个{item_type}吗?\n{filepath}",
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.No
+ )
+
+ if reply == QMessageBox.Yes:
+ try:
+ if is_folder:
+ shutil.rmtree(filepath)
+ else:
+ os.remove(filepath)
+ self._refreshWithStatePreservation()
+ print(f"删除{item_type}: {filepath}")
+ except OSError as e:
+ print(f"删除{item_type}失败: {e}")
+
+ def copyPath(self, filepath):
+ """复制路径到剪贴板"""
+ from PyQt5.QtWidgets import QApplication
+
+ clipboard = QApplication.clipboard()
+ clipboard.setText(filepath)
+ print(f"已复制路径: {filepath}")
+
+ def openFile(self, filepath):
+ """打开文件"""
+ import os
+ import subprocess
+ import platform
+
+ try:
+ system = platform.system()
+ if system == "Windows":
+ os.startfile(filepath)
+ elif system == "Darwin": # macOS
+ subprocess.run(["open", filepath])
+ else: # Linux
+ subprocess.run(["xdg-open", filepath])
+ print(f"打开文件: {filepath}")
+ except Exception as e:
+ print(f"打开文件失败: {e}")
+
+ def showProperties(self, item):
+ """显示属性面板"""
+ import os
+ from PyQt5.QtWidgets import QMessageBox
+
+ filepath = item.data(0, Qt.UserRole)
+ is_folder = item.data(0, Qt.UserRole + 1)
+
+ try:
+ stat = os.stat(filepath)
+ size = stat.st_size
+ modified = os.path.getmtime(filepath)
+
+ import datetime
+ modified_str = datetime.datetime.fromtimestamp(modified).strftime('%Y-%m-%d %H:%M:%S')
+
+ item_type = "文件夹" if is_folder else "文件"
+ size_str = f"{size} 字节" if not is_folder else "文件夹"
+
+ properties = f"""
+ 路径: {filepath}
+ 类型: {item_type}
+ 大小: {size_str}
+ 修改时间: {modified_str}
+ """
+
+ QMessageBox.information(self, "属性", properties.strip())
+
+ except OSError as e:
+ QMessageBox.warning(self, "错误", f"无法获取属性: {e}")
+
+ # def mouseDoubleClickEvent(self, event):
+ # """处理双击事件"""
+ # item = self.itemAt(event.pos())
+ # if item:
+ # filepath = item.data(0, Qt.UserRole)
+ # is_folder = item.data(0, Qt.UserRole + 1)
+ #
+ # if is_folder:
+ # # 文件夹:展开/折叠
+ # item.setExpanded(not item.isExpanded())
+ # else:
+ # # 文件:检查是否是模型文件
+ # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
+ # self.world.importModel(filepath)
+ # else:
+ # # 其他文件:用系统默认程序打开
+ # self.openFile(filepath)
+ #
+ # super().mouseDoubleClickEvent(event)
+
+ def setupUI(self):
+ """初始化UI设置"""
+ self.setHeaderHidden(True)
+ # 启用多选和拖拽
+ self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
+ self.setDropIndicatorShown(True) # 启用拖放指示线
+
+ def setupDragDrop(self):
+ """设置拖拽功能"""
+ # 使用InternalMove模式以正确显示插入指示线
+ self.setDragDropMode(QAbstractItemView.InternalMove)
+ self.setDefaultDropAction(Qt.DropAction.MoveAction)
+ self.setDragEnabled(True)
+ self.setAcceptDrops(True)
+ self.setDropIndicatorShown(True)
+
+ def getProjectRootPath(self):
+ """获取项目根路径下的Resources文件夹,考虑跨平台"""
+ import os
+
+ # 获取项目根路径
+ project_root = os.getcwd()
+
+ # 构建Resources文件夹路径(跨平台)
+ resources_path = os.path.join(project_root, "Resources")
+
+ # 如果Resources文件夹不存在,创建它
+ if not os.path.exists(resources_path):
+ try:
+ os.makedirs(resources_path, exist_ok=True)
+ print(f"创建Resources文件夹: {resources_path}")
+ except OSError as e:
+ print(f"无法创建Resources文件夹: {e}")
+ # 如果无法创建,回退到项目根路径
+ return project_root
+
+ return resources_path
+
+ def load_file_tree(self):
+ """加载树形视图"""
+ self.clear()
+ self.current_path = self.getProjectRootPath()
+ try:
+ # 创建当前目录的根节点
+ root_name = os.path.basename(self.current_path) or self.current_path
+ root_item = QTreeWidgetItem([f"📁 {root_name}"])
+ root_item.setData(0, Qt.UserRole, self.current_path)
+ root_item.setData(0, Qt.UserRole + 1, True)
+ self.addTopLevelItem(root_item)
+
+ # 加载当前目录内容
+ self.load_directory_tree(self.current_path, root_item)
+
+ # 展开根节点
+ root_item.setExpanded(True)
+
+ except PermissionError:
+ error_item = QTreeWidgetItem(["❌ 无权限访问此目录"])
+ self.addTopLevelItem(error_item)
+
+ def load_directory_tree(self, path, parent_item, max_depth=3, current_depth=0):
+ """递归加载目录树(类似左侧导航面板)"""
+ if current_depth >= max_depth:
+ return
+
+ try:
+ items = os.listdir(path)
+ items.sort()
+
+ # 分别处理文件夹和文件
+ folders = []
+ files = []
+
+ for item in items:
+ # 跳过隐藏文件和系统文件
+ if item.startswith('.') or item.startswith('__'):
+ continue
+
+ item_path = os.path.join(path, item)
+ if os.path.isdir(item_path):
+ folders.append(item)
+ elif os.path.isfile(item_path):
+ files.append(item)
+
+ # 先添加文件夹
+ for folder in folders:
+ folder_path = os.path.join(path, folder)
+ folder_item = self.create_simple_tree_item(folder, folder_path, True)
+ parent_item.addChild(folder_item)
+
+ # 递归加载子目录(限制深度)
+ if current_depth < max_depth - 1:
+ self.load_directory_tree(folder_path, folder_item, max_depth, current_depth + 1)
+
+ # 再添加文件(显示重要文件类型,包括图片和模型)
+ important_extensions = {
+ # 编程文件
+ '.py', '.js', '.html', '.css', '.java', '.cpp', '.c', '.php', '.rb', '.go', '.rs',
+ # 文档文件
+ '.md', '.txt', '.pdf', '.doc', '.docx', '.rtf',
+ # 配置和数据文件
+ '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.toml',
+ # 图片文件
+ '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif',
+ # 3D模型文件
+ '.fbx', '.obj', '.3ds', '.max', '.blend', '.dae', '.gltf', '.glb', '.stl', '.ply',
+ # 音视频文件
+ '.mp3', '.wav', '.mp4', '.avi', '.mov', '.wmv', '.flac', '.aac',
+ # 压缩文件
+ '.zip', '.rar', '.7z', '.tar', '.gz',
+ # 材质和纹理
+ '.mtl', '.mat', '.exr', '.hdr', '.hdri', '.dds',
+ # CAD文件
+ '.dwg', '.dxf', '.step', '.stp', '.iges', '.sldprt', '.sldasm',
+ # 其他重要文件
+ '.exe', '.dll', '.bat', '.sh', '.log'
+ }
+
+ # 重要文件名(不依赖扩展名)
+ important_files = {'requirements.txt', 'README.md', 'main.py', 'LICENSE', 'CHANGELOG.md', 'INSTALL.md'}
+
+ for file in files:
+ file_ext = os.path.splitext(file)[1].lower()
+ if file_ext in important_extensions or file in important_files:
+ file_path = os.path.join(path, file)
+ file_item = self.create_simple_tree_item(file, file_path, False)
+ parent_item.addChild(file_item)
+
+ except (OSError, PermissionError):
+ pass
+
+ def create_simple_tree_item(self, name, path, is_folder):
+ """创建简单的树形项目"""
+ try:
+ if is_folder:
+ # 文件夹项目,展开/折叠图标由CSS样式控制
+ item = QTreeWidgetItem([f"📁 {name}"])
+ item.setData(0, Qt.UserRole + 1, True) # 标记为文件夹
+ else:
+ icon = self.get_file_icon(name)
+ item = QTreeWidgetItem([f"{icon} {name}"])
+ item.setData(0, Qt.UserRole + 1, False) # 标记为文件
+
+ item.setData(0, Qt.UserRole, path)
+
+ return item
+
+ except (OSError, PermissionError):
+ item = QTreeWidgetItem([name])
+ item.setData(0, Qt.UserRole, path)
+ return item
+
+ def get_file_icon(self, filename):
+ """
+ 根据文件扩展名获取图标
+
+ 这个函数根据文件的扩展名返回对应的Unicode图标字符。
+ 如果文件类型不在映射表中,返回默认的文档图标。
+
+ 参数:
+ filename (str): 文件名(包含扩展名)
+
+ 返回值:
+ str: 对应的Unicode图标字符
+ """
+ # 获取文件扩展名并转换为小写
+ ext = os.path.splitext(filename)[1].lower()
+
+ # 文件扩展名到图标的映射表
+ icon_map = {
+ # 编程语言文件
+ '.py': '🐍', # Python文件
+ '.js': '⚡', # JavaScript文件
+ '.html': '🌐', # HTML文件
+ '.css': '🎨', # CSS样式文件
+ '.java': '☕', # Java文件
+ '.cpp': '⚙️', # C++文件
+ '.c': '⚙️', # C文件
+ '.h': '📋', # 头文件
+ '.php': '🐘', # PHP文件
+ '.rb': '💎', # Ruby文件
+ '.go': '🐹', # Go文件
+ '.rs': '🦀', # Rust文件
+ '.swift': '🐦', # Swift文件
+ '.kt': '🎯', # Kotlin文件
+
+ # 文档文件
+ '.txt': '📄', # 纯文本文件
+ '.md': '📝', # Markdown文档
+ '.rst': '📝', # reStructuredText文档
+ '.pdf': '📕', # PDF文档
+ '.doc': '📘', # Word文档
+ '.docx': '📘', # Word文档
+ '.rtf': '📄', # RTF文档
+ '.odt': '📄', # OpenDocument文本
+
+ # 数据文件
+ '.json': '📋', # JSON数据文件
+ '.xml': '📋', # XML文件
+ '.yaml': '📋', # YAML文件
+ '.yml': '📋', # YAML文件
+ '.csv': '📊', # CSV表格文件
+ '.xls': '📗', # Excel表格
+ '.xlsx': '📗', # Excel表格
+ '.ods': '📊', # OpenDocument表格
+
+ # 图像文件
+ '.jpg': '🖼️', # JPEG图像
+ '.jpeg': '🖼️', # JPEG图像
+ '.png': '🖼️', # PNG图像
+ '.gif': '🖼️', # GIF图像
+ '.bmp': '🖼️', # BMP图像
+ '.svg': '🎨', # SVG矢量图
+ '.ico': '🎯', # 图标文件
+ '.webp': '🖼️', # WebP图像
+ '.tiff': '🖼️', # TIFF图像
+ '.tif': '🖼️', # TIFF图像
+
+ # 音视频文件
+ '.mp4': '🎬', # MP4视频
+ '.avi': '🎬', # AVI视频
+ '.mov': '🎬', # MOV视频
+ '.wmv': '🎬', # WMV视频
+ '.flv': '🎬', # FLV视频
+ '.webm': '🎬', # WebM视频
+ '.mp3': '🎵', # MP3音频
+ '.wav': '🎵', # WAV音频
+ '.flac': '🎵', # FLAC音频
+ '.aac': '🎵', # AAC音频
+ '.ogg': '🎵', # OGG音频
+
+ # 压缩文件
+ '.zip': '📦', # ZIP压缩包
+ '.rar': '📦', # RAR压缩包
+ '.7z': '📦', # 7Z压缩包
+ '.tar': '📦', # TAR归档
+ '.gz': '📦', # GZIP压缩
+ '.bz2': '📦', # BZIP2压缩
+ '.xz': '📦', # XZ压缩
+
+ # 可执行文件
+ '.exe': '⚙️', # Windows可执行文件
+ '.msi': '📦', # Windows安装包
+ '.deb': '📦', # Debian包
+ '.rpm': '📦', # RPM包
+ '.dmg': '💿', # macOS磁盘镜像
+ '.app': '📱', # macOS应用程序
+
+ # 系统文件
+ '.dll': '🔧', # 动态链接库
+ '.so': '🔧', # 共享库
+ '.dylib': '🔧', # macOS动态库
+ '.lib': '📚', # 静态库
+
+ # 脚本文件
+ '.bat': '📜', # Windows批处理
+ '.cmd': '📜', # Windows命令脚本
+ '.sh': '📜', # Shell脚本
+ '.ps1': '💙', # PowerShell脚本
+ '.vbs': '📜', # VBScript脚本
+
+ # 配置文件
+ '.ini': '⚙️', # INI配置文件
+ '.cfg': '⚙️', # 配置文件
+ '.conf': '⚙️', # 配置文件
+ '.config': '⚙️', # 配置文件
+ '.toml': '⚙️', # TOML配置文件
+
+ # 3D模型文件
+ '.fbx': '🎭', # FBX模型文件
+ '.obj': '🎭', # OBJ模型文件
+ '.3ds': '🎭', # 3DS Max模型
+ '.max': '🎭', # 3DS Max场景
+ '.blend': '🎭', # Blender模型
+ '.dae': '🎭', # COLLADA模型
+ '.gltf': '🎭', # glTF模型
+ '.glb': '🎭', # glTF二进制模型
+ '.x3d': '🎭', # X3D模型
+ '.ply': '🎭', # PLY模型
+ '.stl': '🎭', # STL模型(3D打印)
+ '.off': '🎭', # OFF模型
+ '.3mf': '🎭', # 3MF模型
+ '.amf': '🎭', # AMF模型
+ '.x': '🎭', # DirectX模型
+ '.md2': '🎭', # Quake II模型
+ '.md3': '🎭', # Quake III模型
+ '.mdl': '🎭', # Source引擎模型
+ '.mesh': '🎭', # OGRE模型
+ '.scene': '🎭', # OGRE场景
+ '.ac': '🎭', # AC3D模型
+ '.ase': '🎭', # ASCII Scene Export
+ '.assbin': '🎭', # Assimp二进制
+ '.b3d': '🎭', # Blitz3D模型
+ '.bvh': '🎭', # BioVision层次
+ '.csm': '🎭', # CharacterStudio Motion
+ '.hmp': '🎭', # 3D GameStudio模型
+ '.irr': '🎭', # Irrlicht场景
+ '.irrmesh': '🎭', # Irrlicht网格
+ '.lwo': '🎭', # LightWave对象
+ '.lws': '🎭', # LightWave场景
+ '.ms3d': '🎭', # MilkShape 3D
+ '.nff': '🎭', # Neutral文件格式
+ '.q3o': '🎭', # Quick3D对象
+ '.q3s': '🎭', # Quick3D场景
+ '.raw': '🎭', # RAW三角形
+ '.smd': '🎭', # Valve SMD
+ '.ter': '🎭', # Terragen地形
+ '.uc': '🎭', # Unreal模型
+ '.vta': '🎭', # Valve VTA
+ '.xgl': '🎭', # XGL模型
+ '.zgl': '🎭', # ZGL模型
+
+ # 纹理和材质文件
+ '.mtl': '🎨', # OBJ材质文件
+ '.mat': '🎨', # 材质文件
+ '.sbsar': '🎨', # Substance Archive
+ '.sbs': '🎨', # Substance Designer
+ '.sbsm': '🎨', # Substance材质
+ '.exr': '🖼️', # OpenEXR高动态范围图像
+ '.hdr': '🖼️', # HDR图像
+ '.hdri': '🖼️', # HDRI环境贴图
+ '.dds': '🖼️', # DirectDraw Surface
+ '.ktx': '🖼️', # Khronos纹理
+ '.astc': '🖼️', # ASTC纹理
+ '.pvr': '🖼️', # PowerVR纹理
+ '.etc1': '🖼️', # ETC1纹理
+ '.etc2': '🖼️', # ETC2纹理
+
+ # 动画文件
+ '.anim': '🎬', # 动画文件
+ '.fbx': '🎬', # FBX动画(也可以是模型)
+ '.bip': '🎬', # Character Studio Biped
+ '.cal3d': '🎬', # Cal3D动画
+ '.motion': '🎬', # 动作文件
+ '.mocap': '🎬', # 动作捕捉数据
+
+ # 其他常见文件
+ '.log': '📋', # 日志文件
+ '.tmp': '🗂️', # 临时文件
+ '.bak': '💾', # 备份文件
+ '.old': '📦', # 旧文件
+ }
+
+ # 返回对应的图标,如果找不到则返回默认文档图标
+ return icon_map.get(ext, '📄')
+
+ def startDrag(self, supportedActions):
+ """开始拖拽操作"""
+ selected_items = self.selectedItems()
+ if not selected_items:
+ return
+
+ # 创建 MIME 数据
+ mimeData = QMimeData()
+
+ # 收集文件路径用于向外拖拽
+ urls = []
+ internal_paths = []
+
+ for item in selected_items:
+ filepath = item.data(0, Qt.UserRole)
+ if filepath:
+ internal_paths.append(filepath)
+ # 检查是否是模型文件(用于向外拖拽)
+ if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
+ urls.append(QUrl.fromLocalFile(filepath))
+
+ # 设置内部拖拽数据
+ mimeData.setText('\n'.join(internal_paths))
+
+ # 设置向外拖拽数据
+ if urls:
+ mimeData.setUrls(urls)
+
+ # 创建拖拽对象
+ drag = QDrag(self)
+ drag.setMimeData(mimeData)
+
+ # 设置拖拽图标
+ pixmap = QPixmap(32, 32)
+ pixmap.fill(Qt.transparent)
+ painter = QPainter(pixmap)
+ painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(selected_items)))
+ painter.end()
+ drag.setPixmap(pixmap)
+
+ # 执行拖拽
+ drag.exec_(supportedActions)
+
+ def dragEnterEvent(self, event):
+ """处理拖拽进入事件"""
+ # 检查是否是内部拖拽
+ if event.mimeData().hasText():
+ event.acceptProposedAction()
+ else:
+ event.ignore()
+
+ def dragMoveEvent(self, event):
+ """处理拖拽移动事件"""
+ if not event.mimeData().hasText():
+ event.ignore()
+ return
+
+ # 获取目标项
+ target_item = self.itemAt(event.pos())
+ selected_items = self.selectedItems()
+
+ # 检查是否拖拽到多选区域内的项目
+ if target_item and target_item in selected_items:
+ event.ignore()
+ return
+
+ # 检查是否拖拽到自己的子级
+ if target_item:
+ for selected_item in selected_items:
+ if self._isChildOf(target_item, selected_item):
+ event.ignore()
+ return
+
+ # 如果拖到文件夹,允许拖到文件夹内部
+ if target_item:
+ is_folder = target_item.data(0, Qt.UserRole + 1)
+ if is_folder:
+ # 接受拖放,显示指示框
+ event.acceptProposedAction()
+ return
+
+
+ # 调用父类方法处理插入指示线
+ event.accept()
+ super().dragMoveEvent(event)
+
+ def _isChildOf(self, potential_child, potential_parent):
+ """检查 potential_child 是否是 potential_parent 的子级"""
+ current = potential_child.parent()
+ while current:
+ if current == potential_parent:
+ return True
+ current = current.parent()
+ return False
+
+ def dropEvent(self, event):
+ """处理拖放事件"""
+ if not event.mimeData().hasText():
+ event.ignore()
+ return
+
+ drag_paths = event.mimeData().text().split('\n')
+ if not drag_paths:
+ event.ignore()
+ return
+
+ # 获取目标项
+ target_item = self.itemAt(event.pos())
+ if not target_item:
+ # 如果拖到空白处,默认放到根目录
+ target_path = self.current_path
+ else:
+ # 如果是文件夹,就放到里面
+ is_folder = target_item.data(0, Qt.UserRole + 1)
+ if is_folder:
+ target_path = target_item.data(0, Qt.UserRole)
+ else:
+ # 如果是文件,则放到其父目录
+ parent_item = target_item.parent()
+ if parent_item:
+ target_path = parent_item.data(0, Qt.UserRole)
+ else:
+ target_path = self.current_path
+
+ # 执行移动
+ self._moveFiles(drag_paths, target_path)
+ event.acceptProposedAction()
+ # 让 Qt 更新界面
+ super().dropEvent(event)
+
+ def _moveFiles(self, source_paths, target_dir):
+ """移动文件到目标目录"""
+ import os
+ import shutil
+ from PyQt5.QtWidgets import QMessageBox
+
+ moved_files = []
+ failed_files = []
+
+ for source_path in source_paths:
+ if not source_path or not os.path.exists(source_path):
+ continue
+
+ if os.path.isdir(source_path) and target_dir.startswith(source_path):
+ failed_files.append(f"{source_path} (不能移动到子目录)")
+ continue
+
+ if os.path.dirname(source_path) == target_dir:
+ continue
+
+ filename = os.path.basename(source_path)
+ target_path = os.path.join(target_dir, filename)
+
+ if os.path.exists(target_path):
+ failed_files.append(f"{filename} (目标已存在)")
+ continue
+
+ try:
+ shutil.move(source_path, target_path)
+ moved_files.append(filename)
+ print(f"移动文件: {source_path} -> {target_path}")
+ except Exception as e:
+ failed_files.append(f"{filename} ({str(e)})")
+
+ # 使用状态保持刷新
+ if moved_files:
+ self._refreshWithStatePreservation()
+
+ if failed_files:
+ QMessageBox.warning(
+ self,
+ "移动失败",
+ f"以下文件移动失败:\n" + "\n".join(failed_files)
+ )
+
+ if moved_files:
+ print(f"成功移动 {len(moved_files)} 个文件")
+
+ # def mouseDoubleClickEvent(self, event):
+ # """处理双击事件"""
+ # item = self.itemAt(event.pos())
+ # if item:
+ # filepath = item.data(0, Qt.UserRole)
+ # is_folder = item.data(0, Qt.UserRole + 1)
+ #
+ # if is_folder:
+ # # 文件夹:展开/折叠
+ # item.setExpanded(not item.isExpanded())
+ # else:
+ # # 文件:检查是否是模型文件
+ # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
+ # self.world.importModel(filepath)
+ # else:
+ # # 其他文件:用系统默认程序打开
+ # self.openFile(filepath)
+ #
+ # super().mouseDoubleClickEvent(event)
+
+
+class CustomConsoleDockWidget(QWidget):
+ """自定义控制台停靠部件"""
+
+ def __init__(self, world, parent=None):
+ if parent is None:
+ parent = wrapinstance(0, QWidget)
+ super().__init__(parent)
+ self.world = world
+ self.setupUI()
+ self.setupConsoleRedirect()
+
+ def setupUI(self):
+ """初始化控制台UI"""
+ layout = QVBoxLayout(self)
+
+ # 控制台工具栏
+ toolbar = QHBoxLayout()
+
+ # 清空按钮
+ self.clearBtn = QPushButton("清空")
+ self.clearBtn.clicked.connect(self.clearConsole)
+ toolbar.addWidget(self.clearBtn)
+
+ # 自动滚动开关
+ self.autoScrollBtn = QPushButton("自动滚动")
+ self.autoScrollBtn.setCheckable(True)
+ self.autoScrollBtn.setChecked(True)
+ toolbar.addWidget(self.autoScrollBtn)
+
+ # 时间戳开关
+ self.timestampBtn = QPushButton("显示时间")
+ self.timestampBtn.setCheckable(True)
+ self.timestampBtn.setChecked(True)
+ toolbar.addWidget(self.timestampBtn)
+
+ toolbar.addStretch()
+ layout.addLayout(toolbar)
+
+ # 控制台文本区域
+ from PyQt5.QtWidgets import QTextEdit
+ self.consoleText = QTextEdit()
+ self.consoleText.setReadOnly(True)
+ self.consoleText.setStyleSheet("""
+ QTextEdit {
+ background-color: #1e1e1e;
+ color: #ffffff;
+ font-family: 'Consolas', 'Monaco', monospace;
+ font-size: 10pt;
+ border: 1px solid #3e3e3e;
+ }
+ """)
+ layout.addWidget(self.consoleText)
+
+ # # 命令输入区域
+ # inputLayout = QHBoxLayout()
+ # inputLayout.addWidget(QLabel(">>> "))
+ #
+ # self.commandInput = QLineEdit()
+ # self.commandInput.setStyleSheet("""
+ # QLineEdit {
+ # background-color: #2d2d2d;
+ # color: #ffffff;
+ # font-family: 'Consolas', 'Monaco', monospace;
+ # font-size: 10pt;
+ # border: 1px solid #3e3e3e;
+ # padding: 5px;
+ # }
+ # """)
+ # self.commandInput.returnPressed.connect(self.executeCommand)
+ # inputLayout.addWidget(self.commandInput)
+ #
+ # layout.addLayout(inputLayout)
+
+ # 添加欢迎信息
+ self.addMessage("🎮 编辑器控制台已启动", "INFO")
+
+ def setupConsoleRedirect(self):
+ """设置控制台重定向"""
+ import sys
+
+ # 保存原始的stdout和stderr
+ self.original_stdout = sys.stdout
+ self.original_stderr = sys.stderr
+
+ # 创建自定义输出流
+ sys.stdout = ConsoleRedirect(self, "STDOUT")
+ sys.stderr = ConsoleRedirect(self, "STDERR")
+
+ def addMessage(self, message, msg_type="INFO"):
+ """添加消息到控制台"""
+ import datetime
+
+ # 获取当前时间
+ timestamp = datetime.datetime.now().strftime("%H:%M:%S")
+
+ # 根据消息类型设置颜色
+ color_map = {
+ "INFO": "#ffffff", # 白色
+ "WARNING": "#ffaa00", # 橙色
+ "ERROR": "#ff4444", # 红色
+ "SUCCESS": "#44ff44", # 绿色
+ "STDOUT": "#cccccc", # 浅灰色
+ "STDERR": "#ff6666", # 浅红色
+ }
+
+ color = color_map.get(msg_type, "#ffffff")
+
+ # 构建HTML格式的消息
+ if self.timestampBtn.isChecked():
+ html_message = f'[{timestamp}] {message}'
+ else:
+ html_message = f'{message}'
+
+ # 添加到控制台
+ self.consoleText.append(html_message)
+
+ # 自动滚动到底部
+ if self.autoScrollBtn.isChecked():
+ scrollbar = self.consoleText.verticalScrollBar()
+ scrollbar.setValue(scrollbar.maximum())
+
+ def clearConsole(self):
+ """清空控制台"""
+ self.consoleText.clear()
+ self.addMessage("控制台已清空", "INFO")
+
+ def executeCommand(self):
+ """执行命令"""
+ command = self.commandInput.text().strip()
+ if not command:
+ return
+
+ # 显示输入的命令
+ self.addMessage(f">>> {command}", "INFO")
+ self.commandInput.clear()
+
+ try:
+ # 执行Python命令
+ if hasattr(self.world, 'executeCommand'):
+ result = self.world.executeCommand(command)
+ if result:
+ self.addMessage(str(result), "SUCCESS")
+ else:
+ # 简单的eval执行
+ result = eval(command)
+ if result is not None:
+ self.addMessage(str(result), "SUCCESS")
+
+ except Exception as e:
+ self.addMessage(f"错误: {str(e)}", "ERROR")
+
+ def cleanup(self):
+ """清理资源"""
+ import sys
+
+ # 恢复原始的stdout和stderr
+ if hasattr(self, 'original_stdout'):
+ sys.stdout = self.original_stdout
+ if hasattr(self, 'original_stderr'):
+ sys.stderr = self.original_stderr
+
+
+class ConsoleRedirect:
+ """控制台重定向类"""
+
+ def __init__(self, console_widget, stream_type):
+ self.console_widget = console_widget
+ self.stream_type = stream_type
+
+ def write(self, text):
+ """重定向写入"""
+ if text.strip(): # 忽略空行
+ self.console_widget.addMessage(text.strip(), self.stream_type)
+
+ def flush(self):
+ """刷新缓冲区"""
+ pass
class CustomTreeWidget(QTreeWidget):
"""自定义场景树部件"""
-
+
def __init__(self, world, parent=None):
if parent is None:
parent = wrapinstance(0, QWidget)
super().__init__(parent)
self.world = world
self.setupUI() # 初始化界面
+ self.setupContextMenu() # 初始化右键菜单
self.setupDragDrop() # 设置拖拽功能
@@ -329,10 +1322,82 @@ class CustomTreeWidget(QTreeWidget):
self.setDragEnabled(True)
self.setAcceptDrops(True)
+ def setupContextMenu(self):
+ """设置右键菜单"""
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
+ self.customContextMenuRequested.connect(self.showContextMenu)
+
+ def showContextMenu(self, position):
+ """显示右键菜单 - 复用主菜单的创建动作"""
+ if not self.selectedItems():
+ print("没有选中的项目,不显示右键菜单")
+ return
+
+ item = self.selectedItems()[0]
+ print(f"为项目 '{item.text(0)}' 显示右键菜单")
+
+ # 创建右键菜单
+ menu = QMenu(self)
+
+ # 获取主窗口的创建菜单动作 - 关键修改
+ if hasattr(self.world, 'main_window'):
+ main_window = self.world.main_window
+ create_actions = main_window.getCreateMenuActions()
+
+ # 创建子菜单 - 复用主菜单结构
+ createMenu = menu.addMenu('创建')
+
+ # 基础对象
+ createMenu.addAction(create_actions['createEmpty'])
+
+ # 3D对象菜单
+ create3dObjectMenu = createMenu.addMenu('3D对象')
+
+ # 3D GUI菜单
+ create3dGUIMenu = createMenu.addMenu('3D GUI')
+ create3dGUIMenu.addAction(create_actions['create3DText'])
+
+ # GUI菜单
+ createGUIMenu = createMenu.addMenu('GUI')
+ createGUIMenu.addAction(create_actions['createButton'])
+ createGUIMenu.addAction(create_actions['createLabel'])
+ createGUIMenu.addAction(create_actions['createEntry'])
+ createGUIMenu.addSeparator()
+ createGUIMenu.addAction(create_actions['createVideoScreen'])
+ createGUIMenu.addAction(create_actions['createSphericalVideo'])
+ createGUIMenu.addSeparator()
+ createGUIMenu.addAction(create_actions['createVirtualScreen'])
+
+ # 光源菜单
+ createLightMenu = createMenu.addMenu('光源')
+ createLightMenu.addAction(create_actions['createSpotLight'])
+ createLightMenu.addAction(create_actions['createPointLight'])
+
+ else:
+ # 备用方案:如果无法获取主窗口动作,显示提示
+ createMenu = menu.addMenu('创建')
+ noActionItem = createMenu.addAction('功能不可用')
+ noActionItem.setEnabled(False)
+
+ # 添加删除选项
+ menu.addSeparator()
+ deleteAction = menu.addAction('删除')
+ if hasattr(self, 'delete_items'):
+ deleteAction.triggered.connect(lambda: self.delete_items(self.selectedItems()))
+
+ # 显示菜单
+ global_pos = self.mapToGlobal(position)
+ action = menu.exec_(global_pos)
+
+ print(f"右键菜单已显示在位置: {global_pos}")
+ if action:
+ print(f"用户选择了动作: {action.text()}")
+ else:
+ print("用户取消了菜单选择")
+
def dropEvent(self, event):
dragged_item = self.currentItem()
target_item = self.itemAt(event.pos())
-
if not dragged_item or not target_item:
event.ignore()
return
@@ -347,7 +1412,7 @@ class CustomTreeWidget(QTreeWidget):
if not dragged_node or not target_node:
event.ignore()
return
-
+
# # 检查是否是有效的父子关系
# if self.isValidParentChild(dragged_item, target_item):
# # 保存当前的世界坐标
@@ -364,8 +1429,6 @@ class CustomTreeWidget(QTreeWidget):
# self.world.updatePropertyPanel(dragged_item)
# self.world.property_panel._syncEffectiveVisibility(dragged_node)
-
-
print(f"dragged_node: {dragged_node}, target_node: {target_node}")
# 记录拖拽前的父节点
@@ -390,17 +1453,44 @@ class CustomTreeWidget(QTreeWidget):
world_hpr = dragged_node.getHpr(self.world.render)
world_scale = dragged_node.getScale(self.world.render)
+ # 检查是否是2D GUI元素
+ dragged_type = dragged_item.data(0, Qt.UserRole + 1)
+ is_2d_gui = dragged_type in {"GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_NODE"}
+
# 重新父化到新的父节点
if new_parent_node:
- dragged_node.reparentTo(new_parent_node)
+ if is_2d_gui:
+ # 2D GUI元素需要特殊处理
+ if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1":
+ # 目标是GUI元素,直接重新父化
+ dragged_node.reparentTo(new_parent_node)
+ else:
+ # 目标是3D节点,保持GUI特性,重新父化到aspect2d
+ from direct.showbase.ShowBase import aspect2d
+ dragged_node.reparentTo(aspect2d)
+ print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下")
+ else:
+ # 非GUI元素正常重新父化
+ dragged_node.reparentTo(new_parent_node)
else:
- # 如果新父节点为None,重新父化到render
- dragged_node.reparentTo(self.world.render)
+ # 如果新父节点为None,根据元素类型决定父节点
+ if is_2d_gui:
+ from direct.showbase.ShowBase import aspect2d
+ dragged_node.reparentTo(aspect2d)
+ print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d")
+ else:
+ dragged_node.reparentTo(self.world.render)
- # 恢复世界坐标位置
- dragged_node.setPos(self.world.render, world_pos)
- dragged_node.setHpr(self.world.render, world_hpr)
- dragged_node.setScale(self.world.render, world_scale)
+ # 恢复世界坐标位置(对于2D GUI可能需要调整)
+ if is_2d_gui:
+ # 2D GUI元素使用屏幕坐标系,可能需要特殊处理
+ dragged_node.setPos(world_pos)
+ dragged_node.setHpr(world_hpr)
+ dragged_node.setScale(world_scale)
+ else:
+ dragged_node.setPos(self.world.render, world_pos)
+ dragged_node.setHpr(self.world.render, world_hpr)
+ dragged_node.setScale(self.world.render, world_scale)
print(f"✅ Panda3D父子关系已更新")
else:
@@ -444,15 +1534,16 @@ class CustomTreeWidget(QTreeWidget):
# 检查是否成为了顶级节点
if not item.parent():
- # 如果节点名称不是"场景",说明意外成为了顶级节点
- if item.text(0) != "场景":
+ # 通过数据标识判断是否是场景根节点
+ scene_root_marker = item.data(0, Qt.UserRole + 1)
+ if scene_root_marker != "SCENE_ROOT":
print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...")
# 找到场景根节点
scene_root = None
for i in range(self.topLevelItemCount()):
top_item = self.topLevelItem(i)
- if top_item.text(0) == "场景":
+ if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT":
scene_root = top_item
break
@@ -462,9 +1553,8 @@ class CustomTreeWidget(QTreeWidget):
scene_root.addChild(item)
print(f"✅ 已将节点 {item.text(0)} 移回场景根节点下")
-
def isValidParentChild(self, dragged_item, target_item):
- """检查是否是有效的父子关系(防止循环)"""
+ """检查是否是有效的父子关系(防止循环)+ GUI类型验证"""
# 1. 禁止拖放到自身
if dragged_item == target_item:
@@ -472,13 +1562,14 @@ class CustomTreeWidget(QTreeWidget):
# 2. 禁止拖到根节点之外(根节点本身除外)
target_root = self._getRootNode(target_item)
- if target_root != "场景":
+ if not target_root or target_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT":
print(f"❌ 目标节点 {target_item.text(0)} 不在场景下")
return False
- # 3. 禁止拖拽"场景"根节点
+ # 3. 禁止拖拽场景根节点
dragged_root = self._getRootNode(dragged_item)
- if dragged_item.text(0) == "场景" or dragged_root != "场景":
+ if (dragged_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT" or
+ not dragged_root or dragged_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT"):
print(f"❌ 禁止拖拽场景根节点或根节点外的节点")
return False
@@ -490,14 +1581,156 @@ class CustomTreeWidget(QTreeWidget):
return False
current = current.parent()
+ # 5. GUI元素类型验证 - 新增功能
+ if not self._validateGUITypeCompatibility(dragged_item, target_item):
+ return False
+
return True
+ def _validateGUITypeCompatibility(self, dragged_item, target_item):
+ """验证GUI元素类型兼容性"""
+ try:
+ # 获取节点类型标识
+ dragged_type = dragged_item.data(0, Qt.UserRole + 1)
+ target_type = target_item.data(0, Qt.UserRole + 1)
+
+ # 定义2D GUI元素类型
+ gui_2d_types = {
+ "GUI_BUTTON", # DirectButton
+ "GUI_LABEL", # DirectLabel
+ "GUI_ENTRY", # DirectEntry
+ "GUI_NODE" # 其他2D GUI容器
+ }
+
+ # 定义3D GUI元素类型
+ gui_3d_types = {
+ "GUI_3DTEXT", # 3D TextNode
+ "GUI_VIRTUAL_SCREEN" # Virtual Screen
+ }
+
+ # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素)
+ scene_3d_types = {
+ "SCENE_ROOT",
+ "SCENE_NODE",
+ "LIGHT_NODE",
+ "CAMERA_NODE",
+ "IMPORTED_MODEL_NODE",
+ "MODEL_NODE"
+ }
+
+ # 检查拖拽元素的类型
+ is_dragged_2d_gui = dragged_type in gui_2d_types
+ is_dragged_3d_gui = dragged_type in gui_3d_types
+ is_dragged_3d_scene = dragged_type in scene_3d_types
+
+ # 检查目标的类型
+ is_target_2d_gui = target_type in gui_2d_types
+ is_target_3d_gui = target_type in gui_3d_types
+ is_target_3d_scene = target_type in scene_3d_types
+
+ # === 严格的类型隔离验证逻辑 ===
+
+ # 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下
+ if is_dragged_2d_gui:
+ if is_target_2d_gui:
+ print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}")
+ return True
+ elif is_target_3d_gui:
+ print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D GUI元素 {target_item.text(0)} 下")
+ print(" 💡 提示: 2D GUI元素只能作为其他2D GUI元素的子节点")
+ return False
+ elif is_target_3d_scene:
+ print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D场景节点 {target_item.text(0)} 下")
+ print(" 💡 提示: 2D GUI元素应该保持在2D GUI层级结构中")
+ return False
+ else:
+ print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
+ return False
+
+ # 2. 3D GUI元素的拖拽限制 - 只能拖拽到3D场景节点下
+ elif is_dragged_3d_gui:
+ if is_target_3d_scene:
+ print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
+ return True
+ elif is_target_2d_gui:
+ print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下")
+ print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系")
+ return False
+ elif is_target_3d_gui:
+ print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到其他3D GUI元素 {target_item.text(0)} 下")
+ print(" 💡 提示: 允许3D GUI元素之间建立父子关系")
+ return True
+ else:
+ print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
+ return False
+
+ # 3. 3D场景元素的拖拽限制 - 只能拖拽到3D场景节点或3D GUI元素下
+ elif is_dragged_3d_scene:
+ if is_target_3d_scene:
+ print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
+ return True
+ elif is_target_2d_gui:
+ print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下")
+ print(" 💡 提示: 3D场景元素不能与2D GUI元素建立父子关系")
+ print(" 💡 建议: 将3D场景元素拖拽到其他3D场景节点或场景根节点下")
+ return False
+ elif is_target_3d_gui:
+ print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下")
+ print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下")
+ return True
+ else:
+ print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
+ return False
+
+ # 4. 其他未分类元素 - 严格禁止拖拽到2D GUI下
+ else:
+ if is_target_2d_gui:
+ print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下")
+ print(" 💡 提示: 只有2D GUI元素才能作为其他2D GUI元素的子节点")
+ return False
+ elif is_target_3d_gui or is_target_3d_scene:
+ print(f"✅ 允许元素 {dragged_item.text(0)} 拖拽到3D节点 {target_item.text(0)}")
+ return True
+ else:
+ print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
+ return False
+
+ # 默认情况(理论上不应该到达这里)
+ print(f"⚠️ GUI类型验证遇到未处理的情况: {dragged_type} -> {target_type}")
+ return False
+
+ except Exception as e:
+ print(f"❌ GUI类型兼容性验证失败: {str(e)}")
+ # 出错时采用保守策略,禁止拖拽
+ return False
+
+ def _getNodeTypeDescription(self, node_type):
+ """获取节点类型的描述文本(用于错误提示)"""
+ type_descriptions = {
+ "GUI_BUTTON": "2D按钮",
+ "GUI_LABEL": "2D标签",
+ "GUI_ENTRY": "2D输入框",
+ "GUI_NODE": "2D GUI容器",
+ "GUI_3DTEXT": "3D文本",
+ "GUI_VIRTUAL_SCREEN": "虚拟屏幕",
+ "SCENE_ROOT": "场景根节点",
+ "SCENE_NODE": "场景节点",
+ "LIGHT_NODE": "灯光节点",
+ "CAMERA_NODE": "相机节点",
+ "IMPORTED_MODEL_NODE": "导入模型",
+ "MODEL_NODE": "模型节点"
+ }
+ return type_descriptions.get(node_type, f"未知类型({node_type})")
+
def _getRootNode(self, item):
- """获取树中节点的根节点文本"""
+ """获取树中节点的根节点项"""
+ if not item:
+ return None
+
current = item
while current.parent():
current = current.parent()
- return current.text(0)
+ return current
def dragEnterEvent(self, event):
"""处理拖入事件"""
@@ -554,33 +1787,668 @@ class CustomTreeWidget(QTreeWidget):
super().keyPressEvent(event)
def delete_items(self, selected_items):
- """删除选中的项目"""
+ """删除选中的item - 简化版本"""
if not selected_items:
return
- # 准备确认对话框的内容
- item_count = len(selected_items)
- if item_count == 1:
- item_names = f'"{selected_items[0].text(0)}"'
- title = "确认删除"
- message = f"确定要删除节点 {item_names} 吗?"
- else:
- item_names = "、".join([f'"{item.text(0)}"' for item in selected_items[:3]])
- if item_count > 3:
- item_names += f" 等 {item_count} 个节点"
- title = "确认批量删除"
- message = f"确定要删除以下 {item_count} 个节点吗?\n\n{item_names}"
+ # 过滤掉不能删除的节点
+ deletable_items = []
+ for item in selected_items:
+ node_type = item.data(0, Qt.UserRole + 1)
+ panda_node = item.data(0, Qt.UserRole)
+
+ # 跳过场景根节点和主相机
+ if (node_type == "SCENE_ROOT" or
+ (panda_node and hasattr(self.world, 'cam') and panda_node == self.world.cam)):
+ continue
+ deletable_items.append(item)
+
+ if not deletable_items:
+ QMessageBox.information(self, "提示", "没有可删除的节点")
+ return
+
+ # 确认删除
+ item_count = len(deletable_items)
+ if item_count == 1:
+ message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?"
+ else:
+ message = f"确定要删除 {item_count} 个节点吗?"
- # 创建确认对话框
reply = QMessageBox.question(
- self,
- title,
- message,
- QMessageBox.Yes | QMessageBox.No,
- QMessageBox.No # 默认选择"取消",防止误删
+ self, "确认删除", message,
+ QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
- # 只有用户确认后才执行删除
- if reply == QMessageBox.Yes:
- pass
- print(f"✅ 已删除 {item_count} 个节点")
\ No newline at end of file
+ if reply != QMessageBox.Yes:
+ return
+
+ # 执行删除
+ deleted_count = 0
+ for item in deletable_items:
+ try:
+ panda_node = item.data(0, Qt.UserRole)
+ if panda_node:
+ # 清理选择状态
+ if (hasattr(self.world, 'selection') and
+ hasattr(self.world.selection, 'selectedNode') and
+ self.world.selection.selectedNode == panda_node):
+ self.world.selection.updateSelection(None)
+
+ # 清理特殊资源(参考interface_manager.py的逻辑)
+ if hasattr(self.world, 'property_panel'):
+ self.world.property_panel.removeActorForModel(panda_node)
+
+ # 清理灯光
+ if hasattr(panda_node, 'getPythonTag'):
+ light_object = panda_node.getPythonTag('rp_light_object')
+ if light_object and hasattr(self.world, 'render_pipeline'):
+ self.world.render_pipeline.remove_light(light_object)
+
+ # 从world列表中移除
+ if hasattr(self.world, 'models') and panda_node in self.world.models:
+ self.world.models.remove(panda_node)
+ if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight:
+ self.world.Spotlight.remove(panda_node)
+ if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight:
+ self.world.Pointlight.remove(panda_node)
+
+ # 从Panda3D场景中移除
+ panda_node.removeNode()
+
+ # 从Qt树中移除
+ parent_item = item.parent()
+ if parent_item:
+ parent_item.removeChild(item)
+ else:
+ index = self.indexOfTopLevelItem(item)
+ if index >= 0:
+ self.takeTopLevelItem(index)
+
+ deleted_count += 1
+ print(f"✅ 删除节点: {item.text(0)}")
+
+ except Exception as e:
+ print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}")
+
+ # 最终清理
+ # if hasattr(self.world, 'property_panel'):
+ # self.world.property_panel.clearPropertyPanel()
+
+ print(f"✅ 已删除 {deleted_count} 个节点")
+
+ # ==================== 辅助方法 ====================
+ def _findSceneRoot(self):
+ """查找场景根节点"""
+ for i in range(self.topLevelItemCount()):
+ top_item = self.topLevelItem(i)
+ if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT":
+ return top_item
+ return
+
+ def _generateUniqueNodeName(self, base_name, parent_node):
+ """生成唯一的节点名称"""
+ # 获取父节点下所有子节点的名称
+ existing_names = set()
+ for child in parent_node.getChildren():
+ existing_names.add(child.getName())
+
+ # 如果基础名称不存在,直接使用
+ if base_name not in existing_names:
+ return base_name
+
+ # 否则添加数字后缀
+ counter = 1
+ while f"{base_name}_{counter}" in existing_names:
+ counter += 1
+
+ return f"{base_name}_{counter}"
+
+ def add_node_to_tree_widget(self, node, parent_item, node_type):
+ """将node元素添加到树形控件"""
+
+ BLACK_LIST = {'', '**', 'temp', 'collision'}
+
+ from panda3d.core import CollisionNode
+
+ def should_skip(node):
+ name = node.getName()
+ return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(),CollisionNode) or isinstance(node.node(),ModelRoot) or name==""
+
+ def addNodeToTree(node,parentItem,force = False):
+ if not force and should_skip(node):
+ return
+ nodeItem = QTreeWidgetItem(parentItem,[node.getName()])
+ nodeItem.setData(0,Qt.UserRole, node)
+ nodeItem.setData(0, Qt.UserRole + 1, node_type)
+
+ for child in node.getChildren():
+ addNodeToTree(child,nodeItem,force = False)
+
+ try:
+ from PyQt5.QtWidgets import QTreeWidgetItem
+ from PyQt5.QtCore import Qt
+ if node_type == "IMPORTED_MODEL_NODE":
+ node_name = node.getTag("file") if hasattr(node, 'getTag') else "model"
+ addNodeToTree(node, parent_item, force=True)
+ else:
+ node_name = node.getName() if hasattr(node, 'getName') else "node"
+ new_qt_item = QTreeWidgetItem(parent_item, [node_name])
+ new_qt_item.setData(0, Qt.UserRole, node)
+ new_qt_item.setData(0, Qt.UserRole + 1, node_type)
+
+ # 展开父节点
+ if hasattr(parent_item, 'setExpanded'):
+ parent_item.setExpanded(True)
+
+ print(f"✅ Qt树节点添加成功: {node_name}")
+ return new_qt_item
+
+ except Exception as e:
+ print(f"❌ 添加node到树形控件失败: {str(e)}")
+ return None
+
+ def update_selection_and_properties(self, node, qt_item):
+ """更新选择状态和属性面板"""
+ try:
+ # 更新选择状态
+ if hasattr(self.world, 'selection'):
+ self.world.selection.updateSelection(node)
+
+ # 更新属性面板
+ if hasattr(self.world, 'property_panel'):
+ self.world.property_panel.updatePropertyPanel(qt_item)
+ elif hasattr(self.world, 'updatePropertyPanel'):
+ self.world.updatePropertyPanel(qt_item)
+
+ print(f"✅ 更新选择和属性面板: {qt_item.text(0)}")
+
+ except Exception as e:
+ print(f"❌ 更新选择和属性面板失败: {str(e)}")
+
+ # ==================== 3D辅助方法 ====================
+ def get_target_parents_for_creation(self):
+ """获取创建目标的父节点列表"""
+ from PyQt5.QtCore import Qt
+
+ target_parents = []
+
+ try:
+ selected_items = self.selectedItems()
+
+ # 检查选中的项目,找出所有有效的父节点
+ for item in selected_items:
+ if self._isValidParentForNewNode(item):
+ parent_node = item.data(0, Qt.UserRole)
+ if parent_node:
+ target_parents.append((item, parent_node))
+ print(f"📍 找到有效父节点: {item.text(0)}")
+
+ # 如果没有有效的选中节点,使用场景根节点
+ if not target_parents:
+ print("⚠️ 没有有效选中节点,使用场景根节点")
+ scene_root = self._findSceneRoot()
+ if scene_root:
+ parent_node = scene_root.data(0, Qt.UserRole)
+ if parent_node:
+ target_parents.append((scene_root, parent_node))
+ else:
+ # 如果没有_findSceneRoot方法,使用world.render作为默认父节点
+ if hasattr(self.world, 'render'):
+ # 创建一个虚拟的树项目来表示render节点
+ class MockTreeItem:
+ def text(self, column):
+ return "render"
+
+ mock_item = MockTreeItem()
+ target_parents.append((mock_item, self.world.render))
+
+ print(f"📊 总共找到 {len(target_parents)} 个目标父节点")
+ return target_parents
+
+ except Exception as e:
+ print(f"❌ 获取目标父节点失败: {str(e)}")
+ return []
+
+ def _isValidParentForNewNode(self, item):
+ """检查节点是否适合作为新节点的父节点-3d"""
+ if not item:
+ return False
+
+ # 获取节点类型标识
+ node_type = item.data(0, Qt.UserRole + 1)
+
+ # 场景根节点和普通场景节点可以作为父节点
+ if node_type in ["SCENE_ROOT",
+ "SCENE_NODE", "LIGHT_NODE", "CAMERA_NODE",
+ "IMPORTED_MODEL_NODE",
+ "GUI_3DTEXT"]:
+ return True
+
+ # # 模型节点也可以作为父节点
+ # panda_node = item.data(0, Qt.UserRole)
+ # if panda_node and panda_node.hasTag("is_model_root"):
+ # return True
+ #
+ # # 其他类型的节点也可以,但排除一些特殊情况
+ # if panda_node:
+ # # # 排除相机节点
+ # # if panda_node == self.world.cam:
+ # # return False
+ # # 排除碰撞节点
+ # from panda3d.core import CollisionNode
+ # if isinstance(panda_node.node(), CollisionNode):
+ # return False
+ # return True
+
+ return False
+
+
+ def get_target_parents_for_gui_creation(self):
+ """获取GUI创建目标的父节点列表 - 支持GUI元素作为父节点"""
+ from PyQt5.QtCore import Qt
+
+ target_parents = []
+
+ try:
+ selected_items = self.selectedItems()
+
+ # 检查选中的项目,找出所有有效的父节点
+ for item in selected_items:
+ if self.isValidParentForGUI(item):
+ parent_node = item.data(0, Qt.UserRole)
+ if parent_node:
+ target_parents.append((item, parent_node)) # 修复:确保添加到列表
+ print(f"📍 找到有效GUI父节点: {item.text(0)}")
+ else:
+ print(f"⚠️ GUI父节点 {item.text(0)} 的Panda3D数据为空")
+
+ # 如果没有有效的选中节点,使用场景根节点
+ if not target_parents:
+ print("⚠️ 没有有效选中节点,使用场景根节点")
+ scene_root = self._findSceneRoot()
+ if scene_root:
+ parent_node = scene_root.data(0, Qt.UserRole)
+ if parent_node:
+ target_parents.append((scene_root, parent_node))
+ else:
+ if hasattr(self.world, 'render'):
+ class MockTreeItem:
+ def text(self, column):
+ return "render"
+
+ mock_item = MockTreeItem()
+ target_parents.append((mock_item, self.world.render))
+
+ print(f"📊 总共找到 {len(target_parents)} 个目标父节点")
+ return target_parents
+
+ except Exception as e:
+ print(f"❌ 获取目标父节点失败: {str(e)}")
+ return []
+
+ def isValidParentForGUI(self, item):
+ """检查节点是否适合作为GUI元素的父节点"""
+ if not item:
+ return False
+
+ # 获取节点类型标识
+ node_type = item.data(0, Qt.UserRole + 1)
+
+ # GUI元素可以作为其他GUI元素的父节点
+ if node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_NODE"]:
+ return True
+
+ # 场景根节点和普通场景节点也可以作为父节点
+ if node_type in ["SCENE_ROOT"]:
+ return True
+
+ return False
+
+ def is_gui_element(self, node):
+ """判断节点是否是GUI元素"""
+ if not node:
+ return False
+
+ # 检查是否有GUI标签
+ if hasattr(node, 'getTag'):
+ return node.getTag("is_gui_element") == "1"
+
+ # 检查是否是DirectGUI对象
+ try:
+ from direct.gui.DirectGuiBase import DirectGuiBase
+ return isinstance(node, DirectGuiBase)
+ except ImportError:
+ return False
+
+ def calculate_relative_gui_position(self, pos, parent_gui):
+ """计算相对于GUI父节点的位置"""
+ try:
+ # 对于GUI子元素,使用相对坐标
+ # 这里使用较小的缩放因子,因为是相对于父GUI的位置
+ relative_pos = (pos[0] * 0.05, 0, pos[2] * 0.05)
+ print(f"📐 计算GUI相对位置: {pos} -> {relative_pos}")
+ return relative_pos
+ except Exception as e:
+ print(f"❌ 计算GUI相对位置失败: {str(e)}")
+ # 如果计算失败,返回默认的屏幕坐标
+ return (pos[0] * 0.1, 0, pos[2] * 0.1)
+
+ #---------------------------暂时无用-------------------------------
+
+ def add_existing_node(self, panda_node, node_type="SCENE_NODE", parent_item=None):
+ """将已存在的Panda3D节点添加到Qt树形控件中
+
+ Args:
+ panda_node: 已创建的Panda3D节点
+ node_type: 节点类型标识
+ parent_item: 父Qt项目,如果为None则使用当前选中项或根节点
+
+ Returns:
+ QTreeWidgetItem: 创建的Qt树项目
+ """
+ try:
+ if not panda_node:
+ print("❌ 传入的Panda3D节点为空")
+ return None
+
+ # 确定父项目 - 保持简单,只处理单个父节点
+ if parent_item is None:
+ # 优先使用当前选中的第一个有效节点作为父节点
+ selected_items = self.selectedItems()
+ if selected_items:
+ # 找到第一个有效的父节点
+ for potential_parent in selected_items:
+ if self._isValidParentForNewNode(potential_parent):
+ parent_item = potential_parent
+ print(f"📍 使用选中节点作为父节点: {parent_item.text(0)}")
+ break
+
+ # 如果没有找到有效的选中节点
+ if not parent_item:
+ print("⚠️ 所有选中节点都不适合作为父节点,查找场景根节点")
+ parent_item = self._findSceneRoot()
+ else:
+ # 没有选中任何节点,使用场景根节点
+ print("📍 没有选中节点,使用场景根节点作为父节点")
+ parent_item = self._findSceneRoot()
+
+ # 如果场景根节点也找不到,最后尝试render节点
+ if not parent_item:
+ print("📍 场景根节点未找到,尝试使用render节点")
+ parent_item = self._findRenderItem()
+
+ if not parent_item:
+ print("❌ 无法找到合适的父节点")
+ return None
+
+ # 创建Qt树项目
+ node_name = panda_node.getName()
+ new_qt_item = QTreeWidgetItem(parent_item, [node_name])
+ new_qt_item.setData(0, Qt.UserRole, panda_node)
+ new_qt_item.setData(0, Qt.UserRole + 1, node_type)
+
+ # 展开父节点
+ parent_item.setExpanded(True)
+
+ print(f"✅ 成功将现有节点添加到树形控件: {node_name} -> 父节点: {parent_item.text(0)}")
+ return new_qt_item
+
+ except Exception as e:
+ print(f"❌ 添加现有节点到树形控件失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
+
+ def _findRenderItem(self):
+ """查找render根节点项目"""
+ try:
+ root = self.invisibleRootItem()
+ for i in range(root.childCount()):
+ item = root.child(i)
+ if item.text(0) == "render":
+ return item
+
+ # 如果没找到render节点,返回第一个子项目
+ if root.childCount() > 0:
+ return root.child(0)
+
+ return None
+ except Exception as e:
+ print(f"查找render节点失败: {e}")
+ return None
+
+ def create_item(self, node_type="empty", selected_items=None):
+ """创建不同类型的场景节点
+
+ Args:
+ node_type: 节点类型 ("empty", "spot_light", "point_light")
+ selected_items: 选中的父节点项目列表,如果为None则使用当前选中项或根节点
+ """
+ try:
+ # 确定父节点
+ parent_items = self._determineParentItems(selected_items)
+ if not parent_items:
+ print("⚠️ 无法确定父节点")
+ return []
+
+ created_nodes = []
+
+ for parent_item in parent_items:
+ # 验证父节点的有效性
+ if not self._isValidParentForNewNode(parent_item):
+ print(f"⚠️ 节点 {parent_item.text(0)} 不适合作为父节点")
+ continue
+
+ # 获取父节点的Panda3D对象
+ parent_node = parent_item.data(0, Qt.UserRole)
+ if not parent_node:
+ print(f"⚠️ 父节点 {parent_item.text(0)} 没有对应的Panda3D对象")
+ continue
+
+ # 根据节点类型创建不同的节点
+ if node_type == "empty":
+ new_node = self._createEmptyNode(parent_node, parent_item)
+ elif node_type == "spot_light":
+ new_node = self._createSpotLightNode(parent_node, parent_item)
+ elif node_type == "point_light":
+ new_node = self._createPointLightNode(parent_node, parent_item)
+ else:
+ print(f"❌ 不支持的节点类型: {node_type}")
+ continue
+
+ if new_node:
+ created_nodes.append(new_node)
+
+ # 如果只创建了一个节点,自动选中它
+ if len(created_nodes) == 1:
+ _, qt_item = created_nodes[0]
+ self.setCurrentItem(qt_item)
+ # 更新选择和属性面板
+ if hasattr(self.world, 'selection'):
+ self.world.selection.updateSelection(qt_item.data(0, Qt.UserRole))
+ if hasattr(self.world, 'property_panel'):
+ self.world.property_panel.updatePropertyPanel(qt_item)
+
+ print(f"✅ 总共创建了 {len(created_nodes)} 个 {node_type} 节点")
+ return created_nodes
+
+ except Exception as e:
+ print(f"❌ 创建 {node_type} 节点失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return []
+
+ def _determineParentItems(self, selected_items):
+ """确定父节点项目列表"""
+ if selected_items is not None:
+ return selected_items
+
+ # 使用当前选中的项目
+ current_selected = self.selectedItems()
+ if current_selected:
+ return current_selected
+
+ # 如果没有选中任何项目,使用场景根节点
+ scene_root = self._findSceneRoot()
+ if scene_root:
+ return [scene_root]
+
+ return []
+
+ def _setupNewNodeDefaults(self, node):
+ """设置新节点的默认属性"""
+ # 设置默认位置(相对于父节点)
+ node.setPos(0, 0, 0)
+ node.setHpr(0, 0, 0)
+ node.setScale(1, 1, 1)
+
+ # 设置默认可见性
+ node.show()
+
+ # 可以根据需要添加更多默认设置
+ # 例如:默认材质、碰撞检测等
+
+ # ==================== 创建节点 ====================
+ def _createEmptyNode(self, parent_node, parent_item):
+ """创建空节点"""
+ # 生成唯一的节点名称
+ node_name = self._generateUniqueNodeName("空节点", parent_node)
+
+ # 在Panda3D场景中创建新节点
+ new_panda_node = parent_node.attachNewNode(node_name)
+
+ # 设置新节点的默认属性
+ self._setupNewNodeDefaults(new_panda_node)
+
+ # 设置节点标签
+ new_panda_node.setTag("is_scene_element", "1")
+ new_panda_node.setTag("node_type", "empty_node")
+ new_panda_node.setTag("created_by_user", "1")
+
+ # 在Qt树中创建对应的项目
+ new_qt_item = QTreeWidgetItem(parent_item, [node_name])
+ new_qt_item.setData(0, Qt.UserRole, new_panda_node)
+ new_qt_item.setData(0, Qt.UserRole + 1, "SCENE_NODE")
+
+ # 展开父节点
+ parent_item.setExpanded(True)
+
+ print(f"✅ 成功创建空节点: {node_name}")
+ return (new_panda_node, new_qt_item)
+
+ def _createSpotLightNode(self, parent_node, parent_item):
+ """创建聚光灯节点"""
+ from RenderPipelineFile.rpcore import SpotLight
+ from QPanda3D.Panda3DWorld import get_render_pipeline
+ from panda3d.core import Vec3, NodePath
+
+ try:
+ render_pipeline = get_render_pipeline()
+
+ # 生成唯一的节点名称
+ light_name = self._generateUniqueNodeName(f"Spotlight_{len(self.world.Spotlight)}", parent_node)
+
+ # 创建挂载节点
+ light_np = NodePath(light_name)
+ light_np.reparentTo(parent_node)
+
+ # 创建聚光灯对象
+ light = SpotLight()
+ light.direction = Vec3(0, 0, -1)
+ light.fov = 70
+ light.set_color_from_temperature(5 * 1000.0)
+ light.energy = 5000
+ light.radius = 1000
+ light.casts_shadows = True
+ light.shadow_map_resolution = 256
+ light.setPos(0, 0, 0) # 相对于父节点的位置
+
+ # 添加到渲染管线
+ render_pipeline.add_light(light)
+
+ # 设置节点属性和标签
+ light_np.setTag("light_type", "spot_light")
+ light_np.setTag("is_scene_element", "1")
+ light_np.setTag("light_energy", str(light.energy))
+ light_np.setTag("created_by_user", "1")
+
+ # 保存光源对象引用
+ light_np.setPythonTag("rp_light_object", light)
+
+ # 添加到管理列表
+ self.world.Spotlight.append(light_np)
+
+ # 在Qt树中创建对应的项目
+ new_qt_item = QTreeWidgetItem(parent_item, [light_name])
+ new_qt_item.setData(0, Qt.UserRole, light_np)
+ new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE")
+
+ # 展开父节点
+ parent_item.setExpanded(True)
+
+ print(f"✅ 成功创建聚光灯: {light_name}")
+
+ except Exception as e:
+ print(f"❌ 创建聚光灯失败: {str(e)}")
+ return None
+
+ def _createPointLightNode(self, parent_node, parent_item):
+ """创建点光源节点"""
+ from RenderPipelineFile.rpcore import PointLight
+ from QPanda3D.Panda3DWorld import get_render_pipeline
+ from panda3d.core import Vec3, NodePath
+
+ try:
+ render_pipeline = get_render_pipeline()
+
+ # 生成唯一的节点名称
+ light_name = self._generateUniqueNodeName(f"Pointlight_{len(self.world.Pointlight)}", parent_node)
+
+ # 创建挂载节点
+ light_np = NodePath(light_name)
+ light_np.reparentTo(parent_node)
+
+ # 创建点光源对象
+ light = PointLight()
+ light.setPos(0, 0, 0) # 相对于父节点的位置
+ light.energy = 5000
+ light.radius = 1000
+ light.inner_radius = 0.4
+ light.set_color_from_temperature(5 * 1000.0)
+ light.casts_shadows = True
+ light.shadow_map_resolution = 256
+
+ # 添加到渲染管线
+ render_pipeline.add_light(light)
+
+ # 设置节点属性和标签
+ light_np.setTag("light_type", "point_light")
+ light_np.setTag("is_scene_element", "1")
+ light_np.setTag("light_energy", str(light.energy))
+ light_np.setTag("created_by_user", "1")
+
+ # 保存光源对象引用
+ light_np.setPythonTag("rp_light_object", light)
+
+ # 添加到管理列表
+ self.world.Pointlight.append(light_np)
+
+ # 在Qt树中创建对应的项目
+ new_qt_item = QTreeWidgetItem(parent_item, [light_name])
+ new_qt_item.setData(0, Qt.UserRole, light_np)
+ new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE")
+
+ # 展开父节点
+ parent_item.setExpanded(True)
+
+ print(f"✅ 成功创建点光源: {light_name}")
+ return (light_np, new_qt_item)
+
+ except Exception as e:
+ print(f"❌ 创建点光源失败: {str(e)}")
+ return None
+
+
+
+
+