Merge remote-tracking branch 'origin/main_ch_eg' into addRender
# Conflicts: # gui/gui_manager.py # main.py # scene/scene_manager.py # ui/main_window.py # ui/property_panel.py
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.idea/
|
||||
*.pyc
|
||||
|
||||
@ -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())
|
||||
|
||||
BIN
Resources/icons/test_metallic_stripes.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Resources/icons/test_roughness_checkerboard.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Resources/icons/test_roughness_circle.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
Resources/icons/test_roughness_gradient.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
945
core/collision_manager.py
Normal file
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
BIN
icons/move_tool.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
icons/rotate_tool.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
icons/scale_tool.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
icons/select_tool.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
252
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,38 @@ 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)
|
||||
|
||||
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 +153,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 +199,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 +232,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 +363,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 +371,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 +411,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 +429,7 @@ class MyWorld(CoreWorld):
|
||||
return self.tool_manager.setCurrentTool(tool)
|
||||
|
||||
# ==================== 场景管理功能代理 ====================
|
||||
|
||||
|
||||
# 模型导入和处理方法 - 代理到scene_manager
|
||||
def importModel(self, filepath):
|
||||
"""导入模型到场景"""
|
||||
@ -421,8 +438,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 +452,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 +471,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 +490,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 +530,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 +684,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 +764,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模块的对应功能
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
"""
|
||||
|
||||
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,
|
||||
@ -15,7 +17,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction
|
||||
QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QDialog,
|
||||
QSpinBox)
|
||||
from PyQt5.QtCore import Qt, QDir, QTimer
|
||||
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget
|
||||
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,37 +351,101 @@ class MainWindow(QMainWindow):
|
||||
self.cesiumMenu = menubar.addMenu('Cesium')
|
||||
self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles')
|
||||
self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset)
|
||||
|
||||
|
||||
|
||||
# 帮助菜单
|
||||
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)
|
||||
|
||||
# 创建滚动区域并设置属性
|
||||
@ -200,81 +461,109 @@ class MainWindow(QMainWindow):
|
||||
# 设置属性面板到世界对象
|
||||
self.world.setPropertyLayout(self.propertyLayout)
|
||||
|
||||
|
||||
# 创建脚本管理停靠窗口
|
||||
self.scriptDock = QDockWidget("脚本管理", self)
|
||||
self.scriptDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
|
||||
self.setupScriptPanel()
|
||||
|
||||
# 创建脚本面板的主容器和布局(与属性面板相同结构)
|
||||
self.scriptContainer = QWidget()
|
||||
self.scriptContainer.setObjectName("ScriptContainer")
|
||||
self.scriptLayout = QVBoxLayout(self.scriptContainer)
|
||||
|
||||
# 创建滚动区域并设置属性
|
||||
self.scriptScrollArea = QScrollArea()
|
||||
self.scriptScrollArea.setWidgetResizable(True)
|
||||
self.scriptScrollArea.setWidget(self.scriptContainer)
|
||||
|
||||
# 设置滚动区域为停靠窗口的主部件
|
||||
self.scriptDock.setWidget(self.scriptScrollArea)
|
||||
self.scriptDock.setMinimumWidth(300)
|
||||
|
||||
# 设置脚本面板内容
|
||||
self.setupScriptPanel(self.scriptLayout)
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock)
|
||||
|
||||
|
||||
# 将右侧停靠窗口设为标签形式
|
||||
self.tabifyDockWidget(self.rightDock, self.scriptDock)
|
||||
|
||||
|
||||
# # 创建底部停靠窗口(资源窗口)
|
||||
# self.bottomDock = QDockWidget("资源", self)
|
||||
# self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea)
|
||||
#
|
||||
# # 创建文件系统模型
|
||||
# self.fileModel = QFileSystemModel()
|
||||
# self.fileModel.setRootPath(QDir.homePath()) # 设置为用户主目录
|
||||
#
|
||||
# # 创建树形视图显示文件系统
|
||||
# self.fileView = CustomFileView(self.world)
|
||||
# self.fileView.setModel(self.fileModel)
|
||||
# self.fileView.setRootIndex(self.fileModel.index(QDir.homePath())) # 设置为用户主目录索引
|
||||
#
|
||||
# # 设置列宽
|
||||
# self.fileView.setColumnWidth(0, 250) # 名称列
|
||||
# self.fileView.setColumnWidth(1, 100) # 大小列
|
||||
# self.fileView.setColumnWidth(2, 100) # 类型列
|
||||
# self.fileView.setColumnWidth(3, 150) # 修改日期列
|
||||
#
|
||||
# # 设置视图属性
|
||||
# self.fileView.setMinimumHeight(200) # 设置最小高度
|
||||
#
|
||||
# self.bottomDock.setWidget(self.fileView)
|
||||
# self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
|
||||
|
||||
|
||||
# 创建底部停靠窗口(资源窗口)
|
||||
self.bottomDock = QDockWidget("资源", self)
|
||||
self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea)
|
||||
|
||||
# 创建文件系统模型
|
||||
self.fileModel = QFileSystemModel()
|
||||
self.fileModel.setRootPath(QDir.homePath()) # 设置为用户主目录
|
||||
|
||||
# 创建树形视图显示文件系统
|
||||
self.fileView = CustomFileView(self.world)
|
||||
self.fileView.setModel(self.fileModel)
|
||||
self.fileView.setRootIndex(self.fileModel.index(QDir.homePath())) # 设置为用户主目录索引
|
||||
|
||||
# 设置列宽
|
||||
self.fileView.setColumnWidth(0, 250) # 名称列
|
||||
self.fileView.setColumnWidth(1, 100) # 大小列
|
||||
self.fileView.setColumnWidth(2, 100) # 类型列
|
||||
self.fileView.setColumnWidth(3, 150) # 修改日期列
|
||||
|
||||
# 设置视图属性
|
||||
self.fileView.setMinimumHeight(200) # 设置最小高度
|
||||
|
||||
self.fileView = CustomAssetsTreeWidget(self.world)
|
||||
self.bottomDock.setWidget(self.fileView)
|
||||
self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock)
|
||||
|
||||
|
||||
# 创建底部停靠控制台
|
||||
self.consoleDock = QDockWidget("控制台", self)
|
||||
self.consoleView = CustomConsoleDockWidget(self.world)
|
||||
self.consoleDock.setWidget(self.consoleView)
|
||||
self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
|
||||
|
||||
|
||||
def setupToolbar(self):
|
||||
"""创建工具栏"""
|
||||
self.toolbar = self.addToolBar('工具栏')
|
||||
|
||||
|
||||
# 创建工具按钮组
|
||||
self.toolGroup = QButtonGroup()
|
||||
|
||||
|
||||
# 选择工具
|
||||
self.selectTool = QToolButton()
|
||||
self.selectTool.setText("选择")
|
||||
self.selectTool.setCheckable(True)
|
||||
self.toolGroup.addButton(self.selectTool)
|
||||
self.toolbar.addWidget(self.selectTool)
|
||||
|
||||
|
||||
# 旋转工具
|
||||
self.rotateTool = QToolButton()
|
||||
self.rotateTool.setText("旋转")
|
||||
self.rotateTool.setCheckable(True)
|
||||
self.toolGroup.addButton(self.rotateTool)
|
||||
self.toolbar.addWidget(self.rotateTool)
|
||||
|
||||
|
||||
# 缩放工具
|
||||
self.scaleTool = QToolButton()
|
||||
self.scaleTool.setText("缩放")
|
||||
self.scaleTool.setCheckable(True)
|
||||
self.toolGroup.addButton(self.scaleTool)
|
||||
self.toolbar.addWidget(self.scaleTool)
|
||||
|
||||
|
||||
# 添加分隔符
|
||||
self.toolbar.addSeparator()
|
||||
|
||||
|
||||
# GUI创建工具
|
||||
self.createButtonTool = QToolButton()
|
||||
self.createButtonTool.setText("创建按钮")
|
||||
self.toolbar.addWidget(self.createButtonTool)
|
||||
|
||||
|
||||
self.createLabelTool = QToolButton()
|
||||
self.createLabelTool.setText("创建标签")
|
||||
self.toolbar.addWidget(self.createLabelTool)
|
||||
|
||||
|
||||
self.create3DTextTool = QToolButton()
|
||||
self.create3DTextTool.setText("3D文本")
|
||||
self.toolbar.addWidget(self.create3DTextTool)
|
||||
@ -329,32 +618,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 +647,7 @@ class MainWindow(QMainWindow):
|
||||
self.scriptNameEdit.setPlaceholderText("输入脚本名称...")
|
||||
nameLayout.addWidget(self.scriptNameEdit)
|
||||
createLayout.addLayout(nameLayout)
|
||||
|
||||
|
||||
# 模板选择
|
||||
templateLayout = QHBoxLayout()
|
||||
templateLayout.addWidget(QLabel("模板:"))
|
||||
@ -370,90 +655,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))
|
||||
@ -490,7 +772,7 @@ class MainWindow(QMainWindow):
|
||||
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())
|
||||
@ -505,14 +787,41 @@ class MainWindow(QMainWindow):
|
||||
self.createHeightmapTerrainTool.clicked.connect(self.onCreateHeightmapTerrain)
|
||||
self.terrainEditTool.clicked.connect(self.onTerrainEditMode)
|
||||
|
||||
# # 连接创建事件
|
||||
# # 连接光源创建按钮事件
|
||||
# self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
|
||||
# self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
|
||||
# # 连接GUI创建按钮事件
|
||||
# self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
|
||||
# self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
|
||||
# self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
|
||||
# self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
|
||||
# #self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
|
||||
#
|
||||
# # self.createVideoScreen.triggered.connect(lambda: self.world.video_manager.create_video_screen(
|
||||
# # pos=(0, 0, 2),
|
||||
# # size=(4, 3),
|
||||
# # name=f"video_screen_{len(self.world.video_manager.video_objects) if hasattr(self.world, 'video_manager') else 0}"
|
||||
# # ))
|
||||
# # self.createSphericalVideo.triggered.connect(lambda: self.world.createGUISphericalVideo())
|
||||
#
|
||||
# self.createVirtualScreenAction.triggered.connect(lambda: self.world.create_spherical_video())
|
||||
#
|
||||
# # 连接工具栏GUI创建按钮事件
|
||||
# self.createButtonTool.clicked.connect(lambda: self.world.createGUIButton())
|
||||
# self.createLabelTool.clicked.connect(lambda: self.world.createGUILabel())
|
||||
# self.create3DTextTool.clicked.connect(lambda: self.world.createGUI3DText())
|
||||
# self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight())
|
||||
# self.createPointLight.clicked.connect(lambda :self.world.createPointLight())
|
||||
|
||||
# 连接树节点点击信号
|
||||
# self.treeWidget.itemClicked.connect(self.world.onTreeItemClicked)
|
||||
self.treeWidget.itemSelectionChanged.connect(lambda :self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0))
|
||||
print("已连接点击信号")
|
||||
|
||||
|
||||
# 连接工具切换信号
|
||||
self.toolGroup.buttonClicked.connect(self.onToolChanged)
|
||||
|
||||
|
||||
# 连接脚本菜单事件
|
||||
self.createScriptAction.triggered.connect(self.onCreateScriptDialog)
|
||||
self.loadScriptAction.triggered.connect(self.onLoadScriptFile)
|
||||
@ -520,6 +829,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 +1059,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 +1087,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 +1096,7 @@ class MainWindow(QMainWindow):
|
||||
self.mountScriptCombo.setEnabled(False)
|
||||
self.mountBtn.setEnabled(False)
|
||||
self.mountedScriptsList.clear()
|
||||
|
||||
|
||||
def updateMountedScriptsList(self, game_object):
|
||||
"""更新已挂载脚本列表"""
|
||||
# 保存当前选中项的脚本名(去除状态前缀)
|
||||
@ -795,7 +1105,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 +1114,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 +1124,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 +1144,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 +1158,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 +1175,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 +1191,7 @@ class MainWindow(QMainWindow):
|
||||
QMessageBox.warning(self, "错误", "脚本文件加载失败!")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "错误", f"加载脚本文件时出错: {str(e)}")
|
||||
|
||||
|
||||
def onReloadAllScripts(self):
|
||||
"""重载所有脚本事件"""
|
||||
try:
|
||||
@ -890,44 +1200,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 +1245,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())
|
||||
|
||||