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
This commit is contained in:
Hector 2025-08-28 16:32:26 +08:00
commit f4b16c36c6
17 changed files with 4736 additions and 723 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
__pycache__/
*.pyc
.idea/
*.pyc

View File

@ -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())

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

945
core/collision_manager.py Normal file
View 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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
icons/rotate_tool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
icons/scale_tool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
icons/select_tool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

252
main.py
View File

@ -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模块的对应功能

File diff suppressed because it is too large Load Diff

View File

@ -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:

View File

@ -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())

File diff suppressed because it is too large Load Diff