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/QPanda3D/Panda3DWorld.py b/QPanda3D/Panda3DWorld.py
index d6a4e3b8..a19f52c0 100644
--- a/QPanda3D/Panda3DWorld.py
+++ b/QPanda3D/Panda3DWorld.py
@@ -53,7 +53,7 @@ class Panda3DWorld(ShowBase):
Panda3DWorld : A class to handle all panda3D world manipulation
"""
- def __init__(self, width=1254, height=729, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1),
+ def __init__(self, width=1380, height=729, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1),
name="qpanda3D"):
global _global_world_instance
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 dad88a63..1f1c921e 100644
--- a/gui/gui_manager.py
+++ b/gui/gui_manager.py
@@ -49,72 +49,228 @@ 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
@@ -321,136 +477,345 @@ class GUIManager:
print(f"✓ 创建3D文本: {text} (世界位置: {pos})")
return textNodePath
-
- def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0):
- from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib
-
- # 参数类型检查和转换
- if isinstance(size, (list, tuple)):
- if len(size) >= 2:
- x_size, y_size = float(size[0]), float(size[1])
- else:
- x_size = y_size = float(size[0]) if size else 1.0
- else:
- x_size = y_size = float(size)
-
- # 创建卡片
- cm = CardMaker('gui_3d_image')
- cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2)
-
- # 创建3D图像节点
- image_node = self.world.render.attachNewNode(cm.generate())
- image_node.setPos(*pos)
-
- # 设置面向摄像机
- #image_node.setBillboardAxis() # 让图像总是面向相机
-
- # 创建支持贴图的材质
- # mat = Material()
- # mat.setName("GUI3DImageMaterial")
- # color = LColor(1, 1, 1, 1)
- # mat.set_base_color(color)
- # mat.set_roughness(0.5)
- # mat.set_metallic(0.0)
- # image_node.set_material(mat)
-
- # 为3D图像创建独立的材质
- material = Material(f"image-material-{len(self.gui_elements)}")
- material.setBaseColor(LColor(1, 1, 1, 1))
- material.setDiffuse(LColor(1, 1, 1, 1))
- material.setAmbient(LColor(0.5, 0.5, 0.5, 1))
- material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0))
- material.setShininess(10.0)
- material.setEmission(LColor(0, 0, 0, 1)) # 无自发光
- image_node.setMaterial(material, 1)
-
- image_node.setTransparency(TransparencyAttrib.MAlpha)
-
- # 如果提供了图像路径,则加载纹理
- if image_path:
- self.update3DImageTexture(image_node, image_path)
-
- # 应用PBR效果(如果可用)
+ """创建2D GUI文本输入框 - 支持多选创建和GUI父子关系,优化版本"""
try:
- if hasattr(self, 'render_pipeline') and self.render_pipeline:
- self.render_pipeline.set_effect(
- image_node,
- "effects/default.yaml",
- {
- "normal_mapping": True,
- "render_gbuffer": True,
- "alpha_testing": False,
- "parallax_mapping": False,
- "render_shadow": False,
- "render_envmap": True,
- "disable_children_effects": True
- },
- 50
- )
- print("✓ GUI 3D图像PBR效果已应用")
+ 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 3D图像PBR效果应用失败: {e}")
+ print(f"❌ 创建GUI输入框过程失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
+ return None
- # 为GUI元素添加标识(效仿3D文本方法)
- image_node.setTag("gui_type", "3d_image")
- image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}")
- if image_path:
- image_node.setTag("gui_image_path", image_path)
- image_node.setTag("is_gui_element", "1")
+ def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
+ """创建3D空间文本 - 支持多选创建,优化版本"""
+ try:
+ from panda3d.core import TextNode
+ from PyQt5.QtCore import Qt
- self.gui_elements.append(image_node)
+ print(f"📄 开始创建3D文本,位置: {pos}, 文本: {text}, 尺寸: {size}")
- # 更新场景树
- if hasattr(self.world, 'updateSceneTree'):
- self.world.updateSceneTree()
+ # 获取树形控件
+ tree_widget = self._get_tree_widget()
+ if not tree_widget:
+ print("❌ 无法访问树形控件")
+ return None
- print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})")
- return image_node
+ # 获取目标父节点列表
+ 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,
@@ -459,20 +824,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):
@@ -926,7 +1292,7 @@ class GUIManager:
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
)
y_pos -= spacing
-
+
# 分隔线
y_pos -= 0.1
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 610c7abf..ed81d628 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
@@ -63,7 +66,10 @@ class MyWorld(CoreWorld):
# 初始化GUI管理系统
self.gui_manager = GUIManager(self)
-
+
+ # 初始化视频管理
+ self.video_manager = VideoManager(self)
+
# 初始化场景管理系统
self.scene_manager = SceneManager(self)
@@ -91,27 +97,47 @@ class MyWorld(CoreWorld):
self.terrain_edit_strength=0.3
self.terrain_edit_operation = "add"
+ # 初始化碰撞管理器
+ 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)
+
+ try:
+ self.property_panel = PropertyPanelManager(self)
+ print("✓ 属性面板管理器初始化完成")
+ except Exception as e:
+ print(f"⚠ 属性面板管理器初始化失败: {e}")
+ import traceback
+ traceback.print_exc()
+ self.property_panel = None
+
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元素列表的兼容性设置器"""
@@ -136,35 +162,35 @@ 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):
"""树形控件的兼容性设置器"""
@@ -182,20 +208,20 @@ class MyWorld(CoreWorld):
self.terrain_manager.terrains = 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)
@@ -215,110 +241,110 @@ class MyWorld(CoreWorld):
def createPointLight(self,pos=(0,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)
@@ -346,7 +372,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)
@@ -354,35 +380,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)
@@ -394,7 +420,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)
@@ -412,7 +438,7 @@ class MyWorld(CoreWorld):
return self.tool_manager.setCurrentTool(tool)
# ==================== 场景管理功能代理 ====================
-
+
# 模型导入和处理方法 - 代理到scene_manager
def importModel(self, filepath):
"""导入模型到场景"""
@@ -421,8 +447,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
@@ -435,17 +461,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):
"""处理模型材质"""
@@ -454,17 +480,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文件"""
@@ -473,39 +499,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
@@ -513,153 +539,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串流已启用")
@@ -667,26 +693,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 {
@@ -747,6 +773,23 @@ class MyWorld(CoreWorld):
return self.terrain_manager.modifyTerrainHeight(terrain_info, x, y, radius, strength, operation)
return False
+ # 添加碰撞管理相关的代理方法
+ 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 3c419e7b..472b2f16 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,
@@ -83,11 +85,11 @@ class SceneManager:
self.cesium_integration = CesiumIntegration(self)
print("✓ 场景管理系统初始化完成")
-
+
# ==================== 模型导入和处理 ====================
def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
- """导入模型到场景
+ """导入模型到场景 - 只在根节点下创建
Args:
filepath: 模型文件路径
@@ -96,14 +98,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)
@@ -120,70 +124,95 @@ class SceneManager:
pass
else:
print(f"⚠️ 转换失败,使用原始文件")
-
- # 总是重新加载模型以确保材质信息完整
- # 不使用ModelPool缓存,避免材质信息丢失问题
+
+ # 直接在render根节点下创建模型
+ print("--- 在根节点下创建模型实例 ---")
+
+ # 加载模型
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)
-
- # 智能缩放标准化(处理FBX子节点的大缩放值)
+ 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=== 开始设置材质 ===")
+ 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):
@@ -244,22 +273,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()):
@@ -269,20 +298,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()
@@ -293,7 +322,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())
@@ -309,7 +338,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())
@@ -318,7 +347,7 @@ class SceneManager:
has_color = True
print(f"{indent}从颜色属性获取: {color}")
break
-
+
# 创建新材质
material = Material()
if has_color:
@@ -329,7 +358,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))
@@ -340,72 +369,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()
@@ -413,52 +442,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:
@@ -469,15 +498,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:
@@ -487,66 +516,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():
@@ -555,15 +584,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):
"""加载带动画的模型
@@ -618,7 +647,7 @@ class SceneManager:
return None
# ==================== 材质和几何体处理 ====================
-
+
def processMaterials(self, model):
"""处理模型材质"""
if isinstance(model.node(), GeomNode):
@@ -628,7 +657,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()):
@@ -639,65 +668,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位作为模型的碰撞掩码
+ """为模型设置碰撞检测(增强版本)"""
+ try:
+ # 创建碰撞节点
+ cNode = CollisionNode(f'modelCollision_{model.getName()}')
- # 获取模型的边界
- bounds = model.getBounds()
- center = bounds.getCenter()
- radius = bounds.getRadius()
+ # 设置碰撞掩码
+ cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择
- # 添加碰撞球体
- cSphere = CollisionSphere(center, radius)
- cNode.addSolid(cSphere)
+ # 如果启用了模型间碰撞检测,添加额外的掩码
+ 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
- # 将碰撞节点附加到模型上
- cNodePath = model.attachNewNode(cNode)
- #cNodePath.hide()
- #cNodePath.show() # 取消注释可以显示碰撞体,用于调试
-
# ==================== 场景树管理 ====================
-
+
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:
# 保存变换信息(关键!)
@@ -708,10 +778,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())
@@ -725,8 +795,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():
@@ -739,53 +809,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"""
@@ -796,7 +866,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"):
@@ -809,14 +879,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"""
@@ -828,52 +898,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:
@@ -938,89 +1008,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(f"Spotlight_{len(self.Spotlight)}")
- light_np.reparentTo(self.world.render)
- light_np.setPos(*pos)
+ # 获取树形控件
+ tree_widget = self._get_tree_widget()
+ if not tree_widget:
+ print("❌ 无法访问树形控件")
+ return None
- # 创建独立的灯光对象
- light = SpotLight()
- light.direction = Vec3(0, 0, -1) # 光照方向
- light.fov = self.lamp_fov if hasattr(self, 'lamp_fov') else 70 # 光源角度
- light.set_color_from_temperature(5 * 1000.0) # 色温(K)
- light.energy = self.half_energy if hasattr(self, 'half_energy') else 5000 # 光照强度
- light.radius = self.lamp_radius if hasattr(self, 'lamp_radius') else 1000 # 影响范围
- light.casts_shadows = True # 是否投射阴影
- light.shadow_map_resolution = 256 # 阴影分辨率
- light.setPos(*pos)
+ # 获取目标父节点列表
+ target_parents = tree_widget.get_target_parents_for_creation()
+ if not target_parents:
+ print("❌ 没有找到有效的父节点")
+ return None
- render_pipeline.add_light(light) # 添加到渲染管线
+ created_lights = []
+ render_pipeline = get_render_pipeline()
- #light_name = f"Spotlight_{len(self.Spotlight)}"
+ # 为每个有效的父节点创建聚光灯
+ for parent_item, parent_node in target_parents:
+ try:
+ # 生成唯一名称
+ light_name = f"Spotlight_{len(self.Spotlight)}"
- #light_np.setName(light_name) # 设置唯一名称
- light_np.setTag("light_type", "spot_light")
- light_np.setTag("is_scene_element", "1")
- light_np.setTag("light_energy", str(light.energy))
+ # 创建挂载节点 - 挂载到选中的父节点
+ light_np = NodePath(light_name)
+ light_np.reparentTo(parent_node) # 挂载到父节点而不是render
+ light_np.setPos(*pos)
- light_np.setPythonTag("rp_light_object", light)
+ # 创建聚光灯对象
+ 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
- self.Spotlight.append(light_np)
+ # 设置光源的世界坐标位置
+ world_pos = light_np.getPos(self.world.render)
+ light.setPos(world_pos)
- if hasattr(self.world, 'updateSceneTree'):
- self.world.updateSceneTree()
+ # 添加到渲染管线
+ render_pipeline.add_light(light)
- return light,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")
+
+ # 保存光源对象引用
+ light_np.setPythonTag("rp_light_object", light)
+
+ # 添加到管理列表
+ 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(f"Pointlight_{len(self.Pointlight)}")
- light_np.reparentTo(self.world.render)
- light_np.setPos(*pos)
+ # 获取树形控件
+ tree_widget = self._get_tree_widget()
+ if not tree_widget:
+ print("❌ 无法访问树形控件")
+ return None
- # 创建独立的灯光对象
- light = PointLight()
- light.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 # 阴影分辨率
+ # 获取目标父节点列表
+ target_parents = tree_widget.get_target_parents_for_creation()
+ if not target_parents:
+ print("❌ 没有找到有效的父节点")
+ return None
- # 添加到渲染管线
- render_pipeline.add_light(light)
+ created_lights = []
+ render_pipeline = get_render_pipeline()
- # 设置标签和Python标签
- light_np.setTag("light_type", "point_light")
- light_np.setTag("is_scene_element", "1")
- light_np.setTag("light_energy", str(light.energy))
+ # 为每个有效的父节点创建点光源
+ for parent_item, parent_node in target_parents:
+ try:
+ # 生成唯一名称
+ light_name = f"Pointlight_{len(self.Pointlight)}"
- # 保存光源对象引用(重要!用于属性面板和删除操作)
- light_np.setPythonTag("rp_light_object", light)
+ # 创建挂载节点 - 挂载到选中的父节点
+ light_np = NodePath(light_name)
+ light_np.reparentTo(parent_node) # 挂载到父节点而不是render
+ light_np.setPos(*pos)
- # 添加到灯光列表
- self.Pointlight.append(light_np)
+ # 创建点光源对象
+ light = PointLight()
- # 更新场景树
- if hasattr(self.world, 'updateSceneTree'):
- self.world.updateSceneTree()
+ # 设置光源的世界坐标位置
+ world_pos = light_np.getPos(self.world.render)
+ light.setPos(world_pos)
- print(f"创建点光源: {light_np.getName()}")
- return light, light_np
+ 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
- # ==================== GLB 转换方法 ====================
+ # 添加到渲染管线
+ 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.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 bca64749..230c23d0 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()
@@ -209,10 +209,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("添加相机节点")
BLACK_LIST = {'','**','temp','collision'}
@@ -254,6 +256,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")
#添加灯光节点
for light in self.world.Spotlight + self.world.Pointlight:
diff --git a/ui/main_window.py b/ui/main_window.py
index 5b8457ab..5946ccc3 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -8,14 +8,16 @@
"""
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, QDialog,
- QSpinBox)
-from PyQt5.QtCore import Qt, QDir, QTimer
-from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget
+ QSpinBox, QFrame)
+from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint
+from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget
class MainWindow(QMainWindow):
"""主窗口类"""
@@ -23,15 +25,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)
@@ -45,6 +49,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)
@@ -55,10 +62,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('新建')
@@ -67,7 +272,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('撤销')
@@ -76,7 +281,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('透视图')
@@ -84,7 +289,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('选择工具')
@@ -94,13 +299,9 @@ 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.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
- self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
+ self.setupCreateMenuActions() # 统一创建菜单动作
self.createGUIaddMenu = self.createMenu.addMenu('GUI')
self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
@@ -124,19 +325,15 @@ class MainWindow(QMainWindow):
self.terrainEditModeAction = self.createTerrainMenu.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)
# 脚本菜单
@@ -154,127 +351,253 @@ class MainWindow(QMainWindow):
self.cesiumMenu = menubar.addMenu('Cesium')
self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles')
self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset)
-
+
+ # 添加地球视图相关菜单项
+ self.createCesiumViewAction = self.cesiumMenu.addAction('创建地球视图')
+ self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView)
+
+ self.toggleCesiumViewAction = self.cesiumMenu.addAction('开关地球视图')
+ self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView)
+
+ self.refreshCesiumViewAction = self.cesiumMenu.addAction('刷新地球视图')
+ self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView)
+
+ # 添加模型相关菜单项
+ self.addModelToCesiumAction = self.cesiumMenu.addAction('添加模型到地球')
+ self.addModelToCesiumAction.triggered.connect(self.onAddModelClicked)
+
+
# 帮助菜单
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)
+ try:
+ self.propertyLayout = QVBoxLayout(self.propertyContainer)
+ print(f"✓ 属性布局创建完成: {self.propertyLayout}")
- # 创建滚动区域并设置属性
- self.scrollArea = QScrollArea()
- self.scrollArea.setWidgetResizable(True)
- self.scrollArea.setWidget(self.propertyContainer)
+ # 添加初始提示信息
+ tipLabel = QLabel("选择对象以查看属性")
+ tipLabel.setStyleSheet("color: gray; padding: 10px;") # 使用灰色字体
+ self.propertyLayout.addWidget(tipLabel)
+ print("✓ 提示标签添加完成")
- # 设置滚动区域为停靠窗口的主部件
- self.rightDock.setWidget(self.scrollArea)
- self.rightDock.setMinimumWidth(300)
- self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock)
+ # 创建滚动区域并设置属性
+ self.scrollArea = QScrollArea()
+ self.scrollArea.setWidgetResizable(True)
+ self.scrollArea.setWidget(self.propertyContainer)
+ print("✓ 滚动区域设置完成")
- # 设置属性面板到世界对象
- self.world.setPropertyLayout(self.propertyLayout)
+ # 设置滚动区域为停靠窗口的主部件
+ self.rightDock.setWidget(self.scrollArea)
+ self.rightDock.setMinimumWidth(300)
+ self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock)
+ print("✓ 右侧停靠窗口添加完成")
+ # 设置属性面板到世界对象
+ if hasattr(self.world, 'setPropertyLayout'):
+ print("开始设置属性布局到世界对象...")
+ self.world.setPropertyLayout(self.propertyLayout)
+ print("✓ 属性布局设置完成")
+ else:
+ print("⚠ 世界对象没有 setPropertyLayout 方法")
+
+ except Exception as e:
+ print(f"✗ 设置属性面板时出错: {e}")
+ import traceback
+ traceback.print_exc()
+
+ # 创建基本的属性面板作为后备方案
+ fallback_widget = QLabel("属性面板初始化失败\n请查看控制台日志")
+ fallback_widget.setStyleSheet("color: red; background-color: white; padding: 10px;")
+ self.rightDock.setWidget(fallback_widget)
+ self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock)
# 创建脚本管理停靠窗口
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)
@@ -329,32 +652,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("脚本名称:"))
@@ -362,7 +681,7 @@ class MainWindow(QMainWindow):
self.scriptNameEdit.setPlaceholderText("输入脚本名称...")
nameLayout.addWidget(self.scriptNameEdit)
createLayout.addLayout(nameLayout)
-
+
# 模板选择
templateLayout = QHBoxLayout()
templateLayout.addWidget(QLabel("模板:"))
@@ -370,90 +689,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))
@@ -461,58 +777,38 @@ class MainWindow(QMainWindow):
self.buildAction.triggered.connect(lambda: buildPackage(self))
self.exitAction.triggered.connect(QApplication.instance().quit)
- #连接工具事件
- self.sunsetAction.triggered.connect(lambda : self.world.setCurrentTool("光照编辑"))
- self.pluginAction.triggered.connect(lambda : self.world.setCurrentTool("图形编辑"))
+ # 连接工具事件
+ self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑"))
+ self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑"))
# 连接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.createSpotLightAction.triggered.connect(lambda: self.world.createSpotLight())
+ self.createPointLightAction.triggered.connect(lambda: self.world.createPointLight())
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())
self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView)
self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView)
self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView)
- self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView)
- self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView)
- self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView)
- #连接地形创建事件
+ # 连接地形创建事件
self.createFlatTerrainAction.triggered.connect(self.onCreateFlatTerrain)
self.createHeightmapTerrainAction.triggered.connect(self.onCreateHeightmapTerrain)
self.terrainEditModeAction.triggered.connect(self.onTerrainEditMode)
-
- # 连接工具栏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.create3DImageTool.clicked.connect(lambda: self.world.createGUI3DImage())
- self.create2DImageTool.clicked.connect(lambda: self.world.createGUI2DImage())
- self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight())
- self.createPointLight.clicked.connect(lambda :self.world.createPointLight())
- # 连接工具栏地形创建按钮事件
- self.createFlatTerrainTool.clicked.connect(self.onCreateFlatTerrain)
- self.createHeightmapTerrainTool.clicked.connect(self.onCreateHeightmapTerrain)
- self.terrainEditTool.clicked.connect(self.onTerrainEditMode)
-
# 连接树节点点击信号
- # self.treeWidget.itemClicked.connect(self.world.onTreeItemClicked)
- self.treeWidget.itemSelectionChanged.connect(lambda :self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0))
+ 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)
@@ -520,6 +816,7 @@ class MainWindow(QMainWindow):
self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload)
self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager)
+
def onCreateCesiumView(self):
if hasattr(self.world,'gui_manager') and self.world.gui_manager:
self.world.gui_manager.createCesiumView()
@@ -749,27 +1046,27 @@ class MainWindow(QMainWindow):
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:
@@ -777,7 +1074,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:
@@ -786,7 +1083,7 @@ class MainWindow(QMainWindow):
self.mountScriptCombo.setEnabled(False)
self.mountBtn.setEnabled(False)
self.mountedScriptsList.clear()
-
+
def updateMountedScriptsList(self, game_object):
"""更新已挂载脚本列表"""
# 保存当前选中项的脚本名(去除状态前缀)
@@ -795,7 +1092,7 @@ class MainWindow(QMainWindow):
if current_item:
# 提取脚本名(移除 "✓ " 或 "✗ " 前缀)
selected_script_name = current_item.text()[2:]
-
+
# 清空并重新填充列表
self.mountedScriptsList.clear()
scripts = self.world.getScripts(game_object)
@@ -804,7 +1101,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()):
@@ -814,16 +1111,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:
@@ -834,7 +1131,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, "创建脚本", "输入脚本名称:")
@@ -848,14 +1145,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)
@@ -865,7 +1162,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(
@@ -881,7 +1178,7 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, "错误", "脚本文件加载失败!")
except Exception as e:
QMessageBox.critical(self, "错误", f"加载脚本文件时出错: {str(e)}")
-
+
def onReloadAllScripts(self):
"""重载所有脚本事件"""
try:
@@ -890,44 +1187,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())
@@ -935,29 +1232,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())
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
+
+
+
+
+