1.修改整体布局(有BUG未修改完成)
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.idea/
|
||||
*.pyc
|
||||
|
||||
@ -121,13 +121,26 @@ class InternalLightManager(object):
|
||||
print("ERROR: Could not detach light, light was not attached!")
|
||||
return
|
||||
|
||||
self._lights.free_slot(light.get_slot())
|
||||
self.gpu_remove_light(light)
|
||||
light.remove_slot()
|
||||
# 保存必要信息,避免过早清理
|
||||
light_slot = light.get_slot()
|
||||
has_shadows = light.get_casts_shadows()
|
||||
|
||||
if 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值
|
||||
|
||||
for i in range(light.get_num_shadow_sources()):
|
||||
# 先发送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())
|
||||
@ -135,11 +148,16 @@ class InternalLightManager(object):
|
||||
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的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()
|
||||
|
||||
def gpu_remove_consecutive_sources(self, first_source, num_sources):
|
||||
cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources)
|
||||
cmd_remove.push_int(first_source.get_slot())
|
||||
|
||||
BIN
Resources/icons/test_metallic_stripes.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Resources/icons/test_roughness_checkerboard.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
Resources/icons/test_roughness_circle.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
Resources/icons/test_roughness_gradient.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
945
core/collision_manager.py
Normal file
@ -0,0 +1,945 @@
|
||||
import time
|
||||
from datetime import datetime
|
||||
from panda3d.core import (
|
||||
CollisionTraverser, CollisionHandlerQueue, CollisionNode,
|
||||
CollisionRay, CollisionSphere, BitMask32, Point3, Vec3
|
||||
)
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
|
||||
class CollisionManager:
|
||||
"""统一的碰撞检测管理器"""
|
||||
|
||||
def __init__(self, world):
|
||||
self.world = world
|
||||
self.traverser = CollisionTraverser("main_traverser")
|
||||
self.queue = CollisionHandlerQueue()
|
||||
|
||||
# 碰撞掩码定义
|
||||
self.MASKS = {
|
||||
# === 基础碰撞掩码定义 ===
|
||||
# 每个掩码使用不同的位(bit)来标识,可以进行位运算组合
|
||||
|
||||
'TERRAIN': BitMask32.bit(0), # 地形/地面 - 通常用于地面碰撞检测
|
||||
'UI_ELEMENT': BitMask32.bit(1), # UI元素 - 界面组件的碰撞检测
|
||||
'CAMERA': BitMask32.bit(2), # 摄像机 - 相机的碰撞检测
|
||||
'MODEL_COLLISION': BitMask32.bit(3), # 模型碰撞 - 通用模型间碰撞检测
|
||||
}
|
||||
|
||||
# 碰撞体形状类型
|
||||
self.COLLISION_SHAPES = {
|
||||
'SPHERE': 'sphere',
|
||||
'BOX': 'box',
|
||||
'CAPSULE': 'capsule',
|
||||
'PLANE': 'plane',
|
||||
'POLYGON': 'polygon',
|
||||
'MESH': 'mesh'
|
||||
}
|
||||
|
||||
# 掩码组合定义
|
||||
self.MASK_COMBINATIONS = {
|
||||
}
|
||||
|
||||
# 碰撞分组管理
|
||||
self.collision_groups = {}
|
||||
self.group_enabled = {}
|
||||
|
||||
# 碰撞回调系统
|
||||
self.collision_callbacks = {}
|
||||
self.collision_filters = {}
|
||||
|
||||
# 空间分割系统(八叉树)
|
||||
self.spatial_partitioning_enabled = False
|
||||
self.octree = None
|
||||
self.octree_max_depth = 6
|
||||
self.octree_max_objects = 10
|
||||
|
||||
# 连续碰撞检测
|
||||
self.ccd_enabled = False
|
||||
self.ccd_threshold = 10.0 # 速度阈值
|
||||
|
||||
# 性能监控
|
||||
self.performance_monitor = CollisionPerformanceMonitor()
|
||||
|
||||
# 模型间碰撞检测配置
|
||||
self.model_collision_enabled = False
|
||||
self.collision_detection_frequency = 0.1
|
||||
self.collision_distance_threshold = 0
|
||||
self.last_collision_check = 0
|
||||
self.collision_history = []
|
||||
self.max_collision_history = 100
|
||||
|
||||
# 碰撞状态跟踪 - 避免重复打印
|
||||
self.active_collisions = set() # 当前活跃的碰撞对
|
||||
|
||||
# 模型间碰撞检测任务
|
||||
self.collision_task = None
|
||||
|
||||
print("✅ 碰撞检测管理器初始化完成")
|
||||
|
||||
def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
|
||||
"""启用/禁用模型间碰撞检测
|
||||
|
||||
Args:
|
||||
enable: 是否启用碰撞检测
|
||||
frequency: 检测频率(秒)
|
||||
threshold: 碰撞距离阈值
|
||||
"""
|
||||
self.model_collision_enabled = enable
|
||||
self.collision_detection_frequency = frequency
|
||||
self.collision_distance_threshold = threshold
|
||||
|
||||
if enable:
|
||||
print(f"✅ 启用模型间碰撞检测 - 频率: {frequency}s, 阈值: {threshold}")
|
||||
self._startCollisionDetectionTask()
|
||||
else:
|
||||
print("❌ 禁用模型间碰撞检测")
|
||||
self._stopCollisionDetectionTask()
|
||||
|
||||
def _startCollisionDetectionTask(self):
|
||||
"""启动碰撞检测任务"""
|
||||
if self.collision_task:
|
||||
taskMgr.remove(self.collision_task)
|
||||
|
||||
def collisionDetectionTask(task):
|
||||
try:
|
||||
self.detectModelCollisions()
|
||||
return task.again
|
||||
except Exception as e:
|
||||
print(f"❌ 碰撞检测任务异常: {str(e)}")
|
||||
return task.again
|
||||
|
||||
self.collision_task = taskMgr.doMethodLater(
|
||||
self.collision_detection_frequency,
|
||||
collisionDetectionTask,
|
||||
"modelCollisionDetection"
|
||||
)
|
||||
|
||||
def _stopCollisionDetectionTask(self):
|
||||
"""停止碰撞检测任务"""
|
||||
if self.collision_task:
|
||||
taskMgr.remove(self.collision_task)
|
||||
self.collision_task = None
|
||||
|
||||
def detectModelCollisions(self, specific_models=None, log_results=True):
|
||||
"""检测模型间碰撞"""
|
||||
if not self.model_collision_enabled and specific_models is None:
|
||||
return []
|
||||
|
||||
start_time = time.perf_counter()
|
||||
current_time = datetime.now()
|
||||
|
||||
models_to_check = specific_models if specific_models else self.world.models
|
||||
if len(models_to_check) < 2:
|
||||
return []
|
||||
|
||||
collision_results = []
|
||||
checked_pairs = set()
|
||||
current_collision_pairs = set()
|
||||
|
||||
# 遍历所有模型对
|
||||
for i, model_a in enumerate(models_to_check):
|
||||
for j, model_b in enumerate(models_to_check[i+1:], i+1):
|
||||
pair_key = tuple(sorted([id(model_a), id(model_b)]))
|
||||
if pair_key in checked_pairs:
|
||||
continue
|
||||
checked_pairs.add(pair_key)
|
||||
|
||||
collision_info = self._checkModelPairCollision(model_a, model_b, current_time)
|
||||
if collision_info:
|
||||
collision_results.append(collision_info)
|
||||
current_collision_pairs.add(pair_key)
|
||||
|
||||
# 只有新碰撞才打印日志
|
||||
if log_results and pair_key not in self.active_collisions:
|
||||
self._logCollisionInfo(collision_info)
|
||||
print(f"🆕 新碰撞检测到!")
|
||||
|
||||
# 检测碰撞结束的情况
|
||||
ended_collisions = self.active_collisions - current_collision_pairs
|
||||
for pair_key in ended_collisions:
|
||||
print(f"✅ 碰撞结束: 模型对 {pair_key}")
|
||||
|
||||
# 更新活跃碰撞状态
|
||||
self.active_collisions = current_collision_pairs
|
||||
|
||||
# 记录性能和历史
|
||||
detection_time = time.perf_counter() - start_time
|
||||
self.performance_monitor.recordModelCollisionDetection(
|
||||
detection_time, len(models_to_check), len(collision_results))
|
||||
|
||||
if collision_results:
|
||||
self.collision_history.extend(collision_results)
|
||||
if len(self.collision_history) > self.max_collision_history:
|
||||
self.collision_history = self.collision_history[-self.max_collision_history:]
|
||||
|
||||
return collision_results
|
||||
|
||||
def _checkModelPairCollision(self, model_a, model_b, timestamp):
|
||||
"""使用Panda3D内置碰撞系统检查两个模型是否碰撞"""
|
||||
try:
|
||||
# 检查模型是否有碰撞节点
|
||||
collision_node_a = self._findCollisionNode(model_a)
|
||||
collision_node_b = self._findCollisionNode(model_b)
|
||||
|
||||
if not collision_node_a or not collision_node_b:
|
||||
return None
|
||||
|
||||
# 使用Panda3D的碰撞遍历器
|
||||
temp_traverser = CollisionTraverser()
|
||||
temp_queue = CollisionHandlerQueue()
|
||||
|
||||
# 设置碰撞检测
|
||||
temp_traverser.addCollider(collision_node_a, temp_queue)
|
||||
|
||||
# 执行碰撞检测
|
||||
temp_traverser.traverse(model_b)
|
||||
|
||||
if temp_queue.getNumEntries() > 0:
|
||||
# 有碰撞,获取碰撞信息
|
||||
entry = temp_queue.getEntry(0)
|
||||
|
||||
center_a = model_a.getPos(self.world.render)
|
||||
center_b = model_b.getPos(self.world.render)
|
||||
distance = (center_b - center_a).length()
|
||||
|
||||
return {
|
||||
'timestamp': timestamp,
|
||||
'model_a': {
|
||||
'name': model_a.getName(),
|
||||
'center': center_a,
|
||||
'radius': 0, # 不再需要手动计算半径
|
||||
'node': model_a
|
||||
},
|
||||
'model_b': {
|
||||
'name': model_b.getName(),
|
||||
'center': center_b,
|
||||
'radius': 0,
|
||||
'node': model_b
|
||||
},
|
||||
'collision_point': entry.getSurfacePoint(self.world.render),
|
||||
'distance': distance,
|
||||
'penetration_depth': 0, # Panda3D会自动处理
|
||||
'collision_normal': entry.getSurfaceNormal(self.world.render)
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 检测模型对碰撞失败 ({model_a.getName()} vs {model_b.getName()}): {str(e)}")
|
||||
return None
|
||||
|
||||
def _findCollisionNode(self, model):
|
||||
"""查找模型的碰撞节点"""
|
||||
for child in model.getChildren():
|
||||
if isinstance(child.node(), CollisionNode):
|
||||
return child
|
||||
return None
|
||||
|
||||
def _logCollisionInfo(self, collision_info):
|
||||
"""输出碰撞信息日志"""
|
||||
timestamp = collision_info['timestamp'].strftime('%H:%M:%S.%f')[:-3]
|
||||
model_a = collision_info['model_a']
|
||||
model_b = collision_info['model_b']
|
||||
|
||||
print(f"🔥 [{timestamp}] 检测到碰撞:")
|
||||
print(f" 模型A: {model_a['name']} (中心: {model_a['center']}, 半径: {model_a['radius']:.2f})")
|
||||
print(f" 模型B: {model_b['name']} (中心: {model_b['center']}, 半径: {model_b['radius']:.2f})")
|
||||
print(f" 碰撞点: {collision_info['collision_point']}")
|
||||
print(f" 中心距离: {collision_info['distance']:.3f}")
|
||||
print(f" 穿透深度: {collision_info['penetration_depth']:.3f}")
|
||||
print(f" 碰撞法向: {collision_info['collision_normal']}")
|
||||
|
||||
def getCollisionHistory(self, limit=None):
|
||||
"""获取碰撞历史记录
|
||||
|
||||
Args:
|
||||
limit: 返回记录数量限制
|
||||
|
||||
Returns:
|
||||
list: 碰撞历史记录
|
||||
"""
|
||||
if limit:
|
||||
return self.collision_history[-limit:]
|
||||
return self.collision_history.copy()
|
||||
|
||||
def clearCollisionHistory(self):
|
||||
"""清除碰撞历史记录"""
|
||||
self.collision_history.clear()
|
||||
print("✅ 碰撞历史记录已清除")
|
||||
|
||||
def getCollisionStatistics(self):
|
||||
"""获取碰撞统计信息"""
|
||||
if not self.collision_history:
|
||||
return {"total_collisions": 0, "unique_pairs": 0, "most_frequent_pair": None}
|
||||
|
||||
# 统计碰撞对
|
||||
collision_pairs = {}
|
||||
for collision in self.collision_history:
|
||||
pair_key = tuple(sorted([
|
||||
collision['model_a']['name'],
|
||||
collision['model_b']['name']
|
||||
]))
|
||||
collision_pairs[pair_key] = collision_pairs.get(pair_key, 0) + 1
|
||||
|
||||
most_frequent_pair = max(collision_pairs.items(), key=lambda x: x[1])
|
||||
|
||||
return {
|
||||
"total_collisions": len(self.collision_history),
|
||||
"unique_pairs": len(collision_pairs),
|
||||
"most_frequent_pair": {
|
||||
"models": most_frequent_pair[0],
|
||||
"count": most_frequent_pair[1]
|
||||
},
|
||||
"collision_pairs": collision_pairs
|
||||
}
|
||||
|
||||
def createCollisionShape(self, model, shape_type='auto', **kwargs):
|
||||
"""为模型创建指定类型的碰撞体
|
||||
|
||||
Args:
|
||||
model: 模型节点
|
||||
shape_type: 碰撞体类型 ('auto', 'sphere', 'box', 'capsule', 'plane', 'polygon')
|
||||
**kwargs: 形状特定参数
|
||||
|
||||
Returns:
|
||||
CollisionSolid: 创建的碰撞体
|
||||
"""
|
||||
from panda3d.core import (
|
||||
CollisionSphere, CollisionBox, CollisionCapsule,
|
||||
CollisionPlane, CollisionPolygon, Plane, Vec3
|
||||
)
|
||||
|
||||
bounds = model.getBounds()
|
||||
if bounds.isEmpty():
|
||||
# 默认小球体
|
||||
return CollisionSphere(Point3(0, 0, 0), 1.0)
|
||||
|
||||
center = bounds.getCenter()
|
||||
radius = bounds.getRadius()
|
||||
|
||||
# 自动选择最适合的形状
|
||||
if shape_type == 'auto':
|
||||
shape_type = self._determineOptimalShape(model, bounds)
|
||||
|
||||
if shape_type == 'sphere':
|
||||
return CollisionSphere(center, kwargs.get('radius', radius))
|
||||
|
||||
elif shape_type == 'box':
|
||||
# 创建包围盒
|
||||
min_point = bounds.getMin()
|
||||
max_point = bounds.getMax()
|
||||
return CollisionBox(min_point, max_point)
|
||||
|
||||
elif shape_type == 'capsule':
|
||||
# 创建胶囊体(适合角色)
|
||||
height = kwargs.get('height', (bounds.getMax().z - bounds.getMin().z))
|
||||
radius = kwargs.get('radius', min(bounds.getRadius() * 0.5, height * 0.3))
|
||||
point_a = Point3(center.x, center.y, bounds.getMin().z + radius)
|
||||
point_b = Point3(center.x, center.y, bounds.getMax().z - radius)
|
||||
return CollisionCapsule(point_a, point_b, radius)
|
||||
|
||||
elif shape_type == 'plane':
|
||||
# 创建平面(适合地面、墙面)
|
||||
normal = kwargs.get('normal', Vec3(0, 0, 1))
|
||||
point = kwargs.get('point', center)
|
||||
plane = Plane(normal, point)
|
||||
return CollisionPlane(plane)
|
||||
|
||||
elif shape_type == 'polygon':
|
||||
# === 创建多边形碰撞体 ===
|
||||
# CollisionPolygon特点:
|
||||
# - 单个平面多边形,所有顶点必须共面
|
||||
# - 适用于:地板、墙面、简单平台等平面区域
|
||||
# - 性能较好,但只能表示平面形状
|
||||
#
|
||||
# Mesh碰撞体特点:
|
||||
# - 由多个三角形组成的复杂3D形状
|
||||
# - 适用于:复杂地形、建筑物、不规则物体
|
||||
# - 性能较差,但可以表示任意复杂形状
|
||||
|
||||
vertices = kwargs.get('vertices', [])
|
||||
if len(vertices) >= 3:
|
||||
# 将顶点转换为Point3对象
|
||||
# 要求:所有顶点必须在同一平面上
|
||||
collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices])
|
||||
return collision_poly
|
||||
else:
|
||||
print("⚠️ 多边形至少需要3个顶点,回退到球体")
|
||||
return CollisionSphere(center, radius)
|
||||
|
||||
def _determineOptimalShape(self, model, bounds):
|
||||
"""根据模型特征自动确定最适合的碰撞体形状"""
|
||||
# 获取模型尺寸比例
|
||||
size = bounds.getMax() - bounds.getMin()
|
||||
max_dim = max(size.x, size.y, size.z)
|
||||
min_dim = min(size.x, size.y, size.z)
|
||||
|
||||
# 根据模型名称和比例判断
|
||||
model_name = model.getName().lower()
|
||||
|
||||
# 角色类模型使用胶囊体
|
||||
if any(keyword in model_name for keyword in ['character', 'human', 'player', 'npc']):
|
||||
return 'capsule'
|
||||
|
||||
# 建筑、箱子类使用包围盒
|
||||
if any(keyword in model_name for keyword in ['building', 'box', 'cube', 'wall', 'door']):
|
||||
return 'box'
|
||||
|
||||
# 地面、平台使用平面
|
||||
if any(keyword in model_name for keyword in ['ground', 'floor', 'platform', 'terrain']):
|
||||
return 'plane'
|
||||
|
||||
# 根据尺寸比例判断
|
||||
aspect_ratio = max_dim / min_dim if min_dim > 0 else 1
|
||||
|
||||
if aspect_ratio > 3: # 细长物体用胶囊体
|
||||
return 'capsule'
|
||||
elif aspect_ratio < 1.5: # 接近球形用球体
|
||||
return 'sphere'
|
||||
else: # 其他用包围盒
|
||||
return 'box'
|
||||
|
||||
def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']):
|
||||
"""创建鼠标射线"""
|
||||
# 组合掩码
|
||||
combined_mask = BitMask32.allOff()
|
||||
for mask_type in mask_types:
|
||||
if mask_type in self.MASKS:
|
||||
combined_mask |= self.MASKS[mask_type]
|
||||
|
||||
# 坐标转换
|
||||
near_point, far_point = self.world.event_handler.robustCoordinateConversion(
|
||||
screen_x, screen_y)
|
||||
|
||||
if not near_point:
|
||||
return None
|
||||
|
||||
# 创建射线节点
|
||||
ray_node = CollisionNode('mouse_ray')
|
||||
ray_node.setFromCollideMask(combined_mask)
|
||||
ray_node.setIntoCollideMask(BitMask32.allOff())
|
||||
|
||||
# 创建射线
|
||||
direction = far_point - near_point
|
||||
direction.normalize()
|
||||
ray = CollisionRay(near_point, direction)
|
||||
ray_node.addSolid(ray)
|
||||
|
||||
return ray_node, near_point, far_point
|
||||
|
||||
def performRaycast(self, ray_node, scene_root=None):
|
||||
"""执行射线检测"""
|
||||
if scene_root is None:
|
||||
scene_root = self.world.render
|
||||
|
||||
# 创建临时节点路径
|
||||
ray_np = self.world.cam.attachNewNode(ray_node)
|
||||
|
||||
try:
|
||||
# 清除之前的结果
|
||||
self.queue.clearEntries()
|
||||
|
||||
# 添加碰撞器
|
||||
self.traverser.addCollider(ray_np, self.queue)
|
||||
|
||||
# 执行遍历
|
||||
start_time = time.perf_counter()
|
||||
self.traverser.traverse(scene_root)
|
||||
detection_time = time.perf_counter() - start_time
|
||||
|
||||
# 记录性能
|
||||
self.performance_monitor.recordDetection(
|
||||
detection_time, self.queue.getNumEntries())
|
||||
|
||||
# 处理结果
|
||||
results = []
|
||||
if self.queue.getNumEntries() > 0:
|
||||
self.queue.sortEntries()
|
||||
|
||||
for i in range(self.queue.getNumEntries()):
|
||||
entry = self.queue.getEntry(i)
|
||||
results.append({
|
||||
'hit_pos': entry.getSurfacePoint(scene_root),
|
||||
'hit_normal': entry.getSurfaceNormal(scene_root),
|
||||
'hit_node': entry.getIntoNodePath(),
|
||||
'distance': entry.getT()
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
finally:
|
||||
# 清理资源
|
||||
self.traverser.removeCollider(ray_np)
|
||||
ray_np.removeNode()
|
||||
|
||||
def getCollisionMask(self, mask_type):
|
||||
"""获取碰撞掩码"""
|
||||
return self.MASKS.get(mask_type, BitMask32.allOff())
|
||||
|
||||
def createCollisionGroup(self, group_name, mask_types, enabled=True):
|
||||
"""创建碰撞分组
|
||||
|
||||
Args:
|
||||
group_name: 分组名称
|
||||
mask_types: 掩码类型列表
|
||||
enabled: 是否启用
|
||||
"""
|
||||
combined_mask = BitMask32.allOff()
|
||||
for mask_type in mask_types:
|
||||
if mask_type in self.MASKS:
|
||||
combined_mask |= self.MASKS[mask_type]
|
||||
|
||||
self.collision_groups[group_name] = {
|
||||
'mask': combined_mask,
|
||||
'types': mask_types,
|
||||
'objects': []
|
||||
}
|
||||
self.group_enabled[group_name] = enabled
|
||||
|
||||
print(f"✅ 创建碰撞分组: {group_name}, 掩码: {mask_types}")
|
||||
|
||||
def addToCollisionGroup(self, group_name, model, collision_node):
|
||||
"""将模型添加到碰撞分组"""
|
||||
if group_name in self.collision_groups:
|
||||
self.collision_groups[group_name]['objects'].append({
|
||||
'model': model,
|
||||
'collision_node': collision_node
|
||||
})
|
||||
|
||||
def enableCollisionGroup(self, group_name, enabled=True):
|
||||
"""启用/禁用碰撞分组"""
|
||||
if group_name in self.group_enabled:
|
||||
self.group_enabled[group_name] = enabled
|
||||
|
||||
# 更新分组中所有对象的碰撞状态
|
||||
if group_name in self.collision_groups:
|
||||
for obj in self.collision_groups[group_name]['objects']:
|
||||
collision_node = obj['collision_node']
|
||||
if enabled:
|
||||
collision_node.show()
|
||||
else:
|
||||
collision_node.hide()
|
||||
|
||||
print(f"{'✅ 启用' if enabled else '❌ 禁用'}碰撞分组: {group_name}")
|
||||
|
||||
def registerCollisionCallback(self, mask_a, mask_b, callback_func, filter_func=None):
|
||||
"""注册碰撞回调函数
|
||||
|
||||
Args:
|
||||
mask_a: 第一个掩码类型
|
||||
mask_b: 第二个掩码类型
|
||||
callback_func: 碰撞回调函数 callback(collision_info)
|
||||
filter_func: 过滤函数 filter(model_a, model_b) -> bool
|
||||
"""
|
||||
key = tuple(sorted([mask_a, mask_b]))
|
||||
if key not in self.collision_callbacks:
|
||||
self.collision_callbacks[key] = []
|
||||
|
||||
self.collision_callbacks[key].append(callback_func)
|
||||
|
||||
if filter_func:
|
||||
if key not in self.collision_filters:
|
||||
self.collision_filters[key] = []
|
||||
self.collision_filters[key].append(filter_func)
|
||||
|
||||
print(f"✅ 注册碰撞回调: {mask_a} <-> {mask_b}")
|
||||
|
||||
def _executeCollisionCallbacks(self, collision_info):
|
||||
"""执行碰撞回调"""
|
||||
model_a = collision_info['model_a']['node']
|
||||
model_b = collision_info['model_b']['node']
|
||||
|
||||
# 获取模型的掩码类型
|
||||
mask_a = self._getModelMaskType(model_a)
|
||||
mask_b = self._getModelMaskType(model_b)
|
||||
|
||||
if mask_a and mask_b:
|
||||
key = tuple(sorted([mask_a, mask_b]))
|
||||
|
||||
# 检查过滤条件
|
||||
if key in self.collision_filters:
|
||||
for filter_func in self.collision_filters[key]:
|
||||
if not filter_func(model_a, model_b):
|
||||
return
|
||||
|
||||
# 执行回调
|
||||
if key in self.collision_callbacks:
|
||||
for callback_func in self.collision_callbacks[key]:
|
||||
try:
|
||||
callback_func(collision_info)
|
||||
except Exception as e:
|
||||
print(f"❌ 碰撞回调执行失败: {e}")
|
||||
|
||||
def _getModelMaskType(self, model):
|
||||
"""获取模型的掩码类型"""
|
||||
# 从模型标签或属性中获取掩码类型
|
||||
if model.hasTag('collision_mask_type'):
|
||||
return model.getTag('collision_mask_type')
|
||||
return None
|
||||
|
||||
def enableSpatialPartitioning(self, enabled=True, max_depth=6, max_objects=10):
|
||||
"""启用空间分割优化
|
||||
|
||||
Args:
|
||||
enabled: 是否启用
|
||||
max_depth: 八叉树最大深度
|
||||
max_objects: 每个节点最大对象数
|
||||
"""
|
||||
self.spatial_partitioning_enabled = enabled
|
||||
self.octree_max_depth = max_depth
|
||||
self.octree_max_objects = max_objects
|
||||
|
||||
if enabled:
|
||||
self._buildOctree()
|
||||
print(f"✅ 启用空间分割优化 - 深度: {max_depth}, 对象数: {max_objects}")
|
||||
else:
|
||||
self.octree = None
|
||||
print("❌ 禁用空间分割优化")
|
||||
|
||||
def _buildOctree(self):
|
||||
"""构建八叉树"""
|
||||
if not hasattr(self.world, 'models') or not self.world.models:
|
||||
return
|
||||
|
||||
# 计算场景边界
|
||||
min_bound = Point3(float('inf'), float('inf'), float('inf'))
|
||||
max_bound = Point3(float('-inf'), float('-inf'), float('-inf'))
|
||||
|
||||
for model in self.world.models:
|
||||
bounds = model.getBounds()
|
||||
if not bounds.isEmpty():
|
||||
model_min = bounds.getMin()
|
||||
model_max = bounds.getMax()
|
||||
|
||||
min_bound.x = min(min_bound.x, model_min.x)
|
||||
min_bound.y = min(min_bound.y, model_min.y)
|
||||
min_bound.z = min(min_bound.z, model_min.z)
|
||||
|
||||
max_bound.x = max(max_bound.x, model_max.x)
|
||||
max_bound.y = max(max_bound.y, model_max.y)
|
||||
max_bound.z = max(max_bound.z, model_max.z)
|
||||
|
||||
# 创建八叉树根节点
|
||||
self.octree = OctreeNode(min_bound, max_bound, 0, self.octree_max_depth, self.octree_max_objects)
|
||||
|
||||
# 将模型插入八叉树
|
||||
for model in self.world.models:
|
||||
self.octree.insert(model)
|
||||
|
||||
def detectModelCollisionsOptimized(self, specific_models=None, log_results=True):
|
||||
"""使用空间分割优化的碰撞检测"""
|
||||
if not self.spatial_partitioning_enabled or not self.octree:
|
||||
return self.detectModelCollisions(specific_models, log_results)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
current_time = datetime.now()
|
||||
|
||||
models_to_check = specific_models if specific_models else self.world.models
|
||||
if len(models_to_check) < 2:
|
||||
return []
|
||||
|
||||
collision_results = []
|
||||
current_collision_pairs = set()
|
||||
|
||||
# 使用八叉树进行优化检测
|
||||
for model in models_to_check:
|
||||
# 获取可能碰撞的模型列表
|
||||
potential_collisions = self.octree.query(model)
|
||||
|
||||
for other_model in potential_collisions:
|
||||
if model == other_model:
|
||||
continue
|
||||
|
||||
pair_key = tuple(sorted([id(model), id(other_model)]))
|
||||
if pair_key in current_collision_pairs:
|
||||
continue
|
||||
|
||||
collision_info = self._checkModelPairCollision(model, other_model, current_time)
|
||||
if collision_info:
|
||||
collision_results.append(collision_info)
|
||||
current_collision_pairs.add(pair_key)
|
||||
|
||||
# 执行回调
|
||||
self._executeCollisionCallbacks(collision_info)
|
||||
|
||||
# 只有新碰撞才打印日志
|
||||
if log_results and pair_key not in self.active_collisions:
|
||||
self._logCollisionInfo(collision_info)
|
||||
print(f"🆕 新碰撞检测到!")
|
||||
|
||||
# 更新活跃碰撞状态
|
||||
ended_collisions = self.active_collisions - current_collision_pairs
|
||||
for pair_key in ended_collisions:
|
||||
print(f"✅ 碰撞结束: 模型对 {pair_key}")
|
||||
|
||||
self.active_collisions = current_collision_pairs
|
||||
|
||||
# 记录性能
|
||||
detection_time = time.perf_counter() - start_time
|
||||
self.performance_monitor.recordModelCollisionDetection(
|
||||
detection_time, len(models_to_check), len(collision_results))
|
||||
|
||||
return collision_results
|
||||
|
||||
def cleanup(self):
|
||||
"""清理碰撞管理器资源"""
|
||||
self._stopCollisionDetectionTask()
|
||||
self.collision_history.clear()
|
||||
self.active_collisions.clear()
|
||||
print("✅ 碰撞管理器已清理")
|
||||
|
||||
def setupAdvancedCollision(self, model, shape_type='auto', mask_type='SELECTABLE',
|
||||
group_name=None, **shape_kwargs):
|
||||
"""高级碰撞设置方法
|
||||
|
||||
Args:
|
||||
model: 模型节点
|
||||
shape_type: 碰撞体形状类型
|
||||
mask_type: 掩码类型
|
||||
group_name: 碰撞分组名称
|
||||
**shape_kwargs: 形状特定参数
|
||||
|
||||
Returns:
|
||||
collision_node_path: 碰撞节点路径
|
||||
"""
|
||||
# 创建碰撞节点
|
||||
cNode = CollisionNode(f'collision_{model.getName()}')
|
||||
|
||||
# 设置掩码
|
||||
if mask_type in self.MASKS:
|
||||
cNode.setIntoCollideMask(self.MASKS[mask_type])
|
||||
# 设置模型标签用于回调识别
|
||||
model.setTag('collision_mask_type', mask_type)
|
||||
|
||||
# 创建碰撞体
|
||||
collision_solid = self.createCollisionShape(model, shape_type, **shape_kwargs)
|
||||
cNode.addSolid(collision_solid)
|
||||
|
||||
# 附加到模型
|
||||
cNodePath = model.attachNewNode(cNode)
|
||||
|
||||
# 添加到分组
|
||||
if group_name:
|
||||
if group_name not in self.collision_groups:
|
||||
self.createCollisionGroup(group_name, [mask_type])
|
||||
self.addToCollisionGroup(group_name, model, cNodePath)
|
||||
|
||||
# 根据调试设置决定是否显示
|
||||
if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
|
||||
cNodePath.show()
|
||||
else:
|
||||
cNodePath.hide()
|
||||
|
||||
print(f"✅ 为 {model.getName()} 设置高级碰撞 - 形状: {shape_type}, 掩码: {mask_type}")
|
||||
return cNodePath
|
||||
|
||||
def example_usage(self):
|
||||
"""碰撞系统使用示例"""
|
||||
print("\n=== 碰撞系统使用示例 ===")
|
||||
|
||||
# 1. 创建碰撞分组
|
||||
self.createCollisionGroup('characters', ['CHARACTER', 'DYNAMIC_OBJECT'])
|
||||
self.createCollisionGroup('environment', ['STATIC_GEOMETRY', 'TERRAIN'])
|
||||
self.createCollisionGroup('pickups', ['PICKUP_ITEM', 'INTERACTIVE'])
|
||||
|
||||
# 2. 注册碰撞回调
|
||||
def character_pickup_callback(collision_info):
|
||||
print(f"角色拾取物品: {collision_info['model_a']['name']} -> {collision_info['model_b']['name']}")
|
||||
|
||||
def character_environment_callback(collision_info):
|
||||
print(f"角色碰撞环境: {collision_info['model_a']['name']} 碰到 {collision_info['model_b']['name']}")
|
||||
|
||||
self.registerCollisionCallback('CHARACTER', 'PICKUP_ITEM', character_pickup_callback)
|
||||
self.registerCollisionCallback('CHARACTER', 'STATIC_GEOMETRY', character_environment_callback)
|
||||
|
||||
# 3. 启用空间分割优化
|
||||
self.enableSpatialPartitioning(enabled=True, max_depth=6, max_objects=8)
|
||||
|
||||
# 4. 为模型设置不同类型的碰撞体
|
||||
# model1 = ... # 假设已有模型
|
||||
# self.setupAdvancedCollision(model1, 'capsule', 'CHARACTER', 'characters')
|
||||
# self.setupAdvancedCollision(model2, 'box', 'PICKUP_ITEM', 'pickups')
|
||||
# self.setupAdvancedCollision(model3, 'plane', 'TERRAIN', 'environment')
|
||||
|
||||
print("✅ 碰撞系统示例配置完成")
|
||||
|
||||
class CollisionPerformanceMonitor:
|
||||
"""碰撞检测性能监控器"""
|
||||
|
||||
def __init__(self):
|
||||
self.detection_times = []
|
||||
self.collision_counts = []
|
||||
self.model_collision_times = [] # 新增:模型间碰撞检测时间
|
||||
self.model_collision_data = [] # 新增:模型间碰撞数据
|
||||
self.max_records = 100
|
||||
|
||||
def recordDetection(self, detection_time, collision_count):
|
||||
"""记录射线检测性能"""
|
||||
self.detection_times.append(detection_time)
|
||||
self.collision_counts.append(collision_count)
|
||||
|
||||
# 限制记录数量
|
||||
if len(self.detection_times) > self.max_records:
|
||||
self.detection_times.pop(0)
|
||||
self.collision_counts.pop(0)
|
||||
|
||||
def recordModelCollisionDetection(self, detection_time, model_count, collision_count):
|
||||
"""记录模型间碰撞检测性能"""
|
||||
self.model_collision_times.append(detection_time)
|
||||
self.model_collision_data.append({
|
||||
'model_count': model_count,
|
||||
'collision_count': collision_count,
|
||||
'timestamp': time.time()
|
||||
})
|
||||
|
||||
# 限制记录数量
|
||||
if len(self.model_collision_times) > self.max_records:
|
||||
self.model_collision_times.pop(0)
|
||||
self.model_collision_data.pop(0)
|
||||
|
||||
def getAverageTime(self):
|
||||
"""获取平均射线检测时间"""
|
||||
if not self.detection_times:
|
||||
return 0
|
||||
return sum(self.detection_times) / len(self.detection_times)
|
||||
|
||||
def getAverageModelCollisionTime(self):
|
||||
"""获取平均模型间碰撞检测时间"""
|
||||
if not self.model_collision_times:
|
||||
return 0
|
||||
return sum(self.model_collision_times) / len(self.model_collision_times)
|
||||
|
||||
def getPerformanceReport(self):
|
||||
"""获取完整性能报告"""
|
||||
report = []
|
||||
|
||||
# 射线检测性能
|
||||
if self.detection_times:
|
||||
avg_time = self.getAverageTime()
|
||||
max_time = max(self.detection_times)
|
||||
avg_collisions = sum(self.collision_counts) / len(self.collision_counts)
|
||||
|
||||
report.append("=== 射线检测性能 ===")
|
||||
report.append(f"平均检测时间: {avg_time*1000:.2f}ms")
|
||||
report.append(f"最大检测时间: {max_time*1000:.2f}ms")
|
||||
report.append(f"平均碰撞数量: {avg_collisions:.1f}")
|
||||
report.append(f"检测次数: {len(self.detection_times)}")
|
||||
|
||||
# 模型间碰撞检测性能
|
||||
if self.model_collision_times:
|
||||
avg_model_time = self.getAverageModelCollisionTime()
|
||||
max_model_time = max(self.model_collision_times)
|
||||
avg_model_count = sum(d['model_count'] for d in self.model_collision_data) / len(self.model_collision_data)
|
||||
total_collisions = sum(d['collision_count'] for d in self.model_collision_data)
|
||||
|
||||
report.append("\n=== 模型间碰撞检测性能 ===")
|
||||
report.append(f"平均检测时间: {avg_model_time*1000:.2f}ms")
|
||||
report.append(f"最大检测时间: {max_model_time*1000:.2f}ms")
|
||||
report.append(f"平均模型数量: {avg_model_count:.1f}")
|
||||
report.append(f"总碰撞次数: {total_collisions}")
|
||||
report.append(f"检测次数: {len(self.model_collision_times)}")
|
||||
|
||||
return "\n".join(report) if report else "没有性能数据"
|
||||
|
||||
class OctreeNode:
|
||||
"""八叉树节点"""
|
||||
|
||||
def __init__(self, min_bound, max_bound, depth, max_depth, max_objects):
|
||||
self.min_bound = min_bound
|
||||
self.max_bound = max_bound
|
||||
self.depth = depth
|
||||
self.max_depth = max_depth
|
||||
self.max_objects = max_objects
|
||||
|
||||
self.objects = []
|
||||
self.children = []
|
||||
self.is_leaf = True
|
||||
|
||||
def insert(self, model):
|
||||
"""插入模型到八叉树"""
|
||||
if not self._contains(model):
|
||||
return False
|
||||
|
||||
if self.is_leaf:
|
||||
self.objects.append(model)
|
||||
|
||||
# 如果超过最大对象数且未达到最大深度,则分割
|
||||
if len(self.objects) > self.max_objects and self.depth < self.max_depth:
|
||||
self._subdivide()
|
||||
|
||||
# 重新分配对象到子节点
|
||||
remaining_objects = []
|
||||
for obj in self.objects:
|
||||
inserted = False
|
||||
for child in self.children:
|
||||
if child.insert(obj):
|
||||
inserted = True
|
||||
break
|
||||
if not inserted:
|
||||
remaining_objects.append(obj)
|
||||
|
||||
self.objects = remaining_objects
|
||||
else:
|
||||
# 尝试插入到子节点
|
||||
inserted = False
|
||||
for child in self.children:
|
||||
if child.insert(model):
|
||||
inserted = True
|
||||
break
|
||||
|
||||
if not inserted:
|
||||
self.objects.append(model)
|
||||
|
||||
return True
|
||||
|
||||
def query(self, model):
|
||||
"""查询可能与指定模型碰撞的模型列表"""
|
||||
if not self._contains(model):
|
||||
return []
|
||||
|
||||
result = list(self.objects)
|
||||
|
||||
if not self.is_leaf:
|
||||
for child in self.children:
|
||||
result.extend(child.query(model))
|
||||
|
||||
return result
|
||||
|
||||
def _contains(self, model):
|
||||
"""检查模型是否在此节点范围内"""
|
||||
bounds = model.getBounds()
|
||||
if bounds.isEmpty():
|
||||
return False
|
||||
|
||||
model_min = bounds.getMin()
|
||||
model_max = bounds.getMax()
|
||||
|
||||
return (model_max.x >= self.min_bound.x and model_min.x <= self.max_bound.x and
|
||||
model_max.y >= self.min_bound.y and model_min.y <= self.max_bound.y and
|
||||
model_max.z >= self.min_bound.z and model_min.z <= self.max_bound.z)
|
||||
|
||||
def _subdivide(self):
|
||||
"""分割节点为8个子节点"""
|
||||
center = Point3(
|
||||
(self.min_bound.x + self.max_bound.x) * 0.5,
|
||||
(self.min_bound.y + self.max_bound.y) * 0.5,
|
||||
(self.min_bound.z + self.max_bound.z) * 0.5
|
||||
)
|
||||
|
||||
# 创建8个子节点
|
||||
subdivisions = [
|
||||
(self.min_bound, center),
|
||||
(Point3(center.x, self.min_bound.y, self.min_bound.z), Point3(self.max_bound.x, center.y, center.z)),
|
||||
(Point3(self.min_bound.x, center.y, self.min_bound.z), Point3(center.x, self.max_bound.y, center.z)),
|
||||
(Point3(center.x, center.y, self.min_bound.z), Point3(self.max_bound.x, self.max_bound.y, center.z)),
|
||||
(Point3(self.min_bound.x, self.min_bound.y, center.z), Point3(center.x, center.y, self.max_bound.z)),
|
||||
(Point3(center.x, self.min_bound.y, center.z), Point3(self.max_bound.x, center.y, self.max_bound.z)),
|
||||
(Point3(self.min_bound.x, center.y, center.z), Point3(center.x, self.max_bound.y, self.max_bound.z)),
|
||||
(center, self.max_bound)
|
||||
]
|
||||
|
||||
for min_b, max_b in subdivisions:
|
||||
child = OctreeNode(min_b, max_b, self.depth + 1, self.max_depth, self.max_objects)
|
||||
self.children.append(child)
|
||||
|
||||
self.is_leaf = False
|
||||
@ -45,11 +45,44 @@ class GUIManager:
|
||||
# ==================== GUI元素创建方法 ====================
|
||||
|
||||
def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1):
|
||||
"""创建2D GUI按钮"""
|
||||
"""创建2D GUI按钮 - 支持多选创建和GUI父子关系,优化版本"""
|
||||
try:
|
||||
from direct.gui.DirectGui import DirectButton
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
# 将3D坐标转换为2D屏幕坐标
|
||||
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,
|
||||
@ -60,29 +93,107 @@ class GUIManager:
|
||||
frameColor=(0.2, 0.6, 0.8, 1),
|
||||
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
|
||||
rolloverSound=None,
|
||||
clickSound=None
|
||||
clickSound=None,
|
||||
parent=parent_gui_node # 设置GUI父节点
|
||||
)
|
||||
|
||||
# 为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)
|
||||
# 安全地调用updateSceneTree
|
||||
if hasattr(self.world, 'updateSceneTree'):
|
||||
self.world.updateSceneTree()
|
||||
|
||||
print(f"✓ 创建GUI按钮: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
|
||||
return 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标签"""
|
||||
"""创建2D GUI标签 - 支持多选创建和GUI父子关系,优化版本"""
|
||||
try:
|
||||
from direct.gui.DirectGui import DirectLabel
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
# 将3D坐标转换为2D屏幕坐标
|
||||
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,
|
||||
@ -90,29 +201,107 @@ class GUIManager:
|
||||
scale=size,
|
||||
frameColor=(0, 0, 0, 0), # 透明背景
|
||||
text_fg=(1, 1, 1, 1),
|
||||
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None
|
||||
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
|
||||
parent=parent_gui_node # 设置GUI父节点
|
||||
)
|
||||
|
||||
# 为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)
|
||||
# 安全地调用updateSceneTree
|
||||
if hasattr(self.world, 'updateSceneTree'):
|
||||
self.world.updateSceneTree()
|
||||
|
||||
print(f"✓ 创建GUI标签: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
|
||||
return 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文本输入框"""
|
||||
"""创建2D GUI文本输入框 - 支持多选创建和GUI父子关系,优化版本"""
|
||||
try:
|
||||
from direct.gui.DirectGui import DirectEntry
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
# 将3D坐标转换为2D屏幕坐标
|
||||
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="",
|
||||
@ -123,26 +312,95 @@ class GUIManager:
|
||||
initialText=placeholder,
|
||||
numLines=1,
|
||||
width=12,
|
||||
focus=0
|
||||
focus=0,
|
||||
parent=parent_gui_node # 设置GUI父节点
|
||||
)
|
||||
|
||||
# 为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)
|
||||
# 安全地调用updateSceneTree
|
||||
if hasattr(self.world, 'updateSceneTree'):
|
||||
self.world.updateSceneTree()
|
||||
|
||||
print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
|
||||
return entry
|
||||
print(f"✅ 为 {parent_item.text(0)} 创建GUI输入框成功: {entry_name}")
|
||||
|
||||
# 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点
|
||||
qt_item = tree_widget.add_node_to_tree_widget(entry, parent_item, "GUI_ENTRY")
|
||||
if qt_item:
|
||||
created_entries.append((entry, qt_item))
|
||||
else:
|
||||
created_entries.append((entry, None))
|
||||
print("⚠️ Qt树节点添加失败,但GUI对象已创建")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 为 {parent_item.text(0)} 创建GUI输入框失败: {str(e)}")
|
||||
continue
|
||||
|
||||
# 处理创建结果
|
||||
if not created_entries:
|
||||
print("❌ 没有成功创建任何GUI输入框")
|
||||
return None
|
||||
|
||||
# 选中最后创建的输入框并更新场景树
|
||||
if created_entries:
|
||||
last_entry, last_qt_item = created_entries[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
tree_widget.update_selection_and_properties(last_entry, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框")
|
||||
|
||||
# 返回值处理
|
||||
if len(created_entries) == 1:
|
||||
return created_entries[0][0]
|
||||
else:
|
||||
return [entry for entry, _ in created_entries]
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 创建GUI输入框过程失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5):
|
||||
"""创建3D空间文本"""
|
||||
"""创建3D空间文本 - 支持多选创建,优化版本"""
|
||||
try:
|
||||
from panda3d.core import TextNode
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
print(f"📄 开始创建3D文本,位置: {pos}, 文本: {text}, 尺寸: {size}")
|
||||
|
||||
# 获取树形控件
|
||||
tree_widget = self._get_tree_widget()
|
||||
if not tree_widget:
|
||||
print("❌ 无法访问树形控件")
|
||||
return None
|
||||
|
||||
# 获取目标父节点列表
|
||||
target_parents = tree_widget.get_target_parents_for_creation()
|
||||
if not target_parents:
|
||||
print("❌ 没有找到有效的父节点")
|
||||
return None
|
||||
|
||||
created_texts = []
|
||||
|
||||
# 为每个有效的父节点创建3D文本
|
||||
for parent_item, parent_node in target_parents:
|
||||
try:
|
||||
# 生成唯一名称
|
||||
text_name = f"GUI3DText_{len(self.gui_elements)}"
|
||||
|
||||
textNode = TextNode(f'3d-text-{len(self.gui_elements)}')
|
||||
textNode.setText(text)
|
||||
@ -150,64 +408,194 @@ class GUIManager:
|
||||
if self.world.getChineseFont():
|
||||
textNode.setFont(self.world.getChineseFont())
|
||||
|
||||
textNodePath = self.world.render.attachNewNode(textNode)
|
||||
# 挂载到选中的父节点
|
||||
textNodePath = parent_node.attachNewNode(textNode)
|
||||
textNodePath.setPos(*pos)
|
||||
textNodePath.setScale(size)
|
||||
textNodePath.setColor(1, 1, 0, 1)
|
||||
textNodePath.setBillboardAxis() # 让文本总是面向相机
|
||||
textNodePath.setName(text_name)
|
||||
|
||||
# 为GUI元素添加标识
|
||||
# 设置节点标签
|
||||
textNodePath.setTag("gui_type", "3d_text")
|
||||
textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}")
|
||||
textNodePath.setTag("gui_text", text)
|
||||
textNodePath.setTag("is_gui_element", "1")
|
||||
textNodePath.setTag("is_scene_element", "1")
|
||||
textNodePath.setTag("created_by_user", "1")
|
||||
|
||||
# 添加到GUI元素列表
|
||||
self.gui_elements.append(textNodePath)
|
||||
# 安全地调用updateSceneTree
|
||||
if hasattr(self.world, 'updateSceneTree'):
|
||||
self.world.updateSceneTree()
|
||||
|
||||
print(f"✓ 创建3D文本: {text} (世界位置: {pos})")
|
||||
return 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虚拟屏幕"""
|
||||
"""创建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)
|
||||
virtualScreen = self.world.render.attachNewNode(cm.generate())
|
||||
virtualScreen.setPos(*pos)
|
||||
virtualScreen.setColor(0.2, 0.2, 0.2, 0.8)
|
||||
virtualScreen.setTransparency(TransparencyAttrib.MAlpha)
|
||||
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)
|
||||
|
||||
# 在虚拟屏幕上添加文本
|
||||
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)
|
||||
screen_text_node = self._create_screen_text(virtual_screen, text, len(self.gui_elements))
|
||||
|
||||
# 为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")
|
||||
# 设置节点标签
|
||||
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")
|
||||
|
||||
self.gui_elements.append(virtualScreen)
|
||||
# 安全地调用updateSceneTree
|
||||
if hasattr(self.world, 'updateSceneTree'):
|
||||
self.world.updateSceneTree()
|
||||
# 添加到GUI元素列表
|
||||
self.gui_elements.append(virtual_screen)
|
||||
|
||||
print(f"✓ 创建虚拟屏幕: {text} (世界位置: {pos})")
|
||||
return virtualScreen
|
||||
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滑块"""
|
||||
@ -232,6 +620,7 @@ class GUIManager:
|
||||
self.gui_elements.append(slider)
|
||||
# 安全地调用updateSceneTree
|
||||
if hasattr(self.world, 'updateSceneTree'):
|
||||
pass # CH
|
||||
self.world.updateSceneTree()
|
||||
|
||||
print(f"✓ 创建GUI滑块: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})")
|
||||
|
||||
BIN
icons/move_tool.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
icons/rotate_tool.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
icons/scale_tool.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
icons/select_tool.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
34
main.py
@ -1,4 +1,7 @@
|
||||
import warnings
|
||||
|
||||
from demo.video_integration import VideoManager
|
||||
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
|
||||
import sys
|
||||
@ -63,6 +66,9 @@ class MyWorld(CoreWorld):
|
||||
# 初始化GUI管理系统
|
||||
self.gui_manager = GUIManager(self)
|
||||
|
||||
# 初始化视频管理
|
||||
self.video_manager = VideoManager(self)
|
||||
|
||||
# 初始化场景管理系统
|
||||
self.scene_manager = SceneManager(self)
|
||||
|
||||
@ -85,7 +91,18 @@ class MyWorld(CoreWorld):
|
||||
|
||||
#self.material_editor = None
|
||||
|
||||
# 初始化碰撞管理器
|
||||
from core.collision_manager import CollisionManager
|
||||
self.collision_manager = CollisionManager(self)
|
||||
|
||||
# 调试选项
|
||||
self.debug_collision = True # 是否显示碰撞体
|
||||
|
||||
# 默认启用模型间碰撞检测(可选)
|
||||
self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5)
|
||||
|
||||
print("✓ MyWorld 初始化完成")
|
||||
print("✅ 碰撞管理器已初始化")
|
||||
|
||||
# ==================== 兼容性属性 ====================
|
||||
|
||||
@ -673,6 +690,23 @@ class MyWorld(CoreWorld):
|
||||
"streaming_status": self.getALVRStreamingStatus() if self.isALVRConnected() else None
|
||||
}
|
||||
|
||||
# 添加碰撞管理相关的代理方法
|
||||
def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5):
|
||||
"""启用模型间碰撞检测"""
|
||||
return self.collision_manager.enableModelCollisionDetection(enable, frequency, threshold)
|
||||
|
||||
def detectModelCollisions(self, specific_models=None, log_results=True):
|
||||
"""检测模型间碰撞"""
|
||||
return self.collision_manager.detectModelCollisions(specific_models, log_results)
|
||||
|
||||
def getCollisionHistory(self, limit=None):
|
||||
"""获取碰撞历史"""
|
||||
return self.collision_manager.getCollisionHistory(limit)
|
||||
|
||||
def getCollisionStatistics(self):
|
||||
"""获取碰撞统计"""
|
||||
return self.collision_manager.getCollisionStatistics()
|
||||
|
||||
|
||||
# ==================== 项目管理功能代理 ====================
|
||||
# 以下函数代理到project_manager模块的对应功能
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from panda3d.core import (
|
||||
ModelPool, ModelRoot, Filename, NodePath, GeomNode, Material, Vec4, Vec3,
|
||||
MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere,
|
||||
@ -33,13 +35,12 @@ class SceneManager:
|
||||
self.Spotlight = []
|
||||
self.Pointlight = []
|
||||
|
||||
|
||||
print("✓ 场景管理系统初始化完成")
|
||||
|
||||
# ==================== 模型导入和处理 ====================
|
||||
|
||||
def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
|
||||
"""导入模型到场景
|
||||
"""导入模型到场景 - 只在根节点下创建
|
||||
|
||||
Args:
|
||||
filepath: 模型文件路径
|
||||
@ -48,14 +49,16 @@ class SceneManager:
|
||||
auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持
|
||||
"""
|
||||
try:
|
||||
print(f"\n=== 开始导入模型: {filepath} ===")
|
||||
print(f"\n💾 开始导入模型: {os.path.basename(filepath)}")
|
||||
print(f"单位转换: {'开启' if apply_unit_conversion else '关闭'}")
|
||||
print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}")
|
||||
print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}")
|
||||
|
||||
# 预处理文件路径和转换
|
||||
filepath = util.normalize_model_path(filepath)
|
||||
original_filepath = filepath
|
||||
|
||||
# 检查是否需要转换为GLB以获得更好的动画支持
|
||||
# 检查是否需要转换为GLB
|
||||
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
|
||||
print(f"🔄 检测到需要转换的格式,尝试转换为GLB...")
|
||||
converted_path = self._convertToGLBWithProgress(filepath)
|
||||
@ -73,69 +76,94 @@ class SceneManager:
|
||||
else:
|
||||
print(f"⚠️ 转换失败,使用原始文件")
|
||||
|
||||
# 总是重新加载模型以确保材质信息完整
|
||||
# 不使用ModelPool缓存,避免材质信息丢失问题
|
||||
# 直接在render根节点下创建模型
|
||||
print("--- 在根节点下创建模型实例 ---")
|
||||
|
||||
# 加载模型
|
||||
print("直接从文件加载模型...")
|
||||
model = self.world.loader.loadModel(filepath)
|
||||
if not model:
|
||||
print("加载模型失败")
|
||||
print("❌ 加载模型失败")
|
||||
return None
|
||||
|
||||
# 设置模型名称
|
||||
model_name = os.path.basename(filepath)
|
||||
model.setName(model_name)
|
||||
|
||||
# 将模型添加到场景
|
||||
# 将模型挂载到render根节点
|
||||
model.reparentTo(self.world.render)
|
||||
# 保存原始路径和转换后的路径
|
||||
|
||||
# 设置标签和路径信息
|
||||
model.setTag("model_path", filepath)
|
||||
model.setTag("original_path", original_filepath)
|
||||
model.setTag("file", model.getName())
|
||||
model.setTag("is_model_root", "1")
|
||||
model.setTag("is_scene_element", "1")
|
||||
model.setTag("created_by_user", "1")
|
||||
|
||||
if filepath != original_filepath:
|
||||
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
|
||||
model.setTag("converted_to_glb", "true")
|
||||
|
||||
# 可选的单位转换(主要针对FBX)
|
||||
# 应用处理选项
|
||||
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
|
||||
print("应用FBX单位转换(厘米到米)...")
|
||||
self._applyUnitConversion(model, 0.01)
|
||||
model.setTag("unit_conversion_applied", "true")
|
||||
|
||||
# 智能缩放标准化(处理FBX子节点的大缩放值)
|
||||
if normalize_scales and filepath.lower().endswith('.fbx'):
|
||||
print("标准化FBX模型缩放层级...")
|
||||
self._normalizeModelScales(model)
|
||||
model.setTag("scale_normalization_applied", "true")
|
||||
|
||||
# 调整模型位置到地面
|
||||
self._adjustModelToGround(model)
|
||||
|
||||
# 创建并设置基础材质
|
||||
print("\n=== 开始设置材质 ===")
|
||||
print("设置材质...")
|
||||
self._applyMaterialsToModel(model)
|
||||
|
||||
# 设置碰撞检测(重要!用于选择功能)
|
||||
print("\n=== 设置碰撞检测 ===")
|
||||
print("设置碰撞检测...")
|
||||
self.setupCollision(model)
|
||||
|
||||
# 添加文件标签用于保存/加载
|
||||
model.setTag("file", model_name)
|
||||
model.setTag("is_model_root", "1")
|
||||
|
||||
# 记录应用的处理选项
|
||||
if apply_unit_conversion:
|
||||
model.setTag("unit_conversion_applied", "true")
|
||||
if normalize_scales:
|
||||
model.setTag("scale_normalization_applied", "true")
|
||||
|
||||
# 添加到模型列表
|
||||
self.models.append(model)
|
||||
|
||||
# 更新场景树
|
||||
self.updateSceneTree()
|
||||
print(f"✅ 创建模型成功: {model.getName()}")
|
||||
|
||||
print(f"=== 模型导入成功: {model_name} ===\n")
|
||||
# 获取树形控件并添加到Qt树中
|
||||
tree_widget = self._get_tree_widget()
|
||||
if tree_widget:
|
||||
# 找到根节点项
|
||||
root_item = None
|
||||
for i in range(tree_widget.topLevelItemCount()):
|
||||
item = tree_widget.topLevelItem(i)
|
||||
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
|
||||
root_item = item
|
||||
break
|
||||
|
||||
if root_item:
|
||||
qt_item = tree_widget.add_node_to_tree_widget(model, root_item, "IMPORTED_MODEL_NODE")
|
||||
if qt_item:
|
||||
tree_widget.setCurrentItem(qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(model, qt_item)
|
||||
print("✅ Qt树节点添加成功")
|
||||
else:
|
||||
print("⚠️ Qt树节点添加失败,但Panda3D对象已创建")
|
||||
else:
|
||||
print("⚠️ 未找到根节点项,无法添加到Qt树")
|
||||
else:
|
||||
print("⚠️ 无法访问树形控件")
|
||||
|
||||
print(f"🎉 模型导入完成")
|
||||
return model
|
||||
|
||||
except Exception as e:
|
||||
print(f"导入模型失败: {str(e)}")
|
||||
print(f"❌ 导入模型过程失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
# def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True):
|
||||
@ -614,25 +642,66 @@ class SceneManager:
|
||||
# ==================== 碰撞系统 ====================
|
||||
|
||||
def setupCollision(self, model):
|
||||
"""为模型设置碰撞检测"""
|
||||
"""为模型设置碰撞检测(增强版本)"""
|
||||
try:
|
||||
# 创建碰撞节点
|
||||
cNode = CollisionNode(f'modelCollision_{model.getName()}')
|
||||
|
||||
# 设置碰撞掩码
|
||||
cNode.setIntoCollideMask(BitMask32.bit(2)) # 使用第2位作为模型的碰撞掩码
|
||||
cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择
|
||||
|
||||
# 如果启用了模型间碰撞检测,添加额外的掩码
|
||||
if (hasattr(self.world, 'collision_manager') and
|
||||
self.world.collision_manager.model_collision_enabled):
|
||||
# 同时设置模型间碰撞掩码
|
||||
current_mask = cNode.getIntoCollideMask()
|
||||
model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION
|
||||
cNode.setIntoCollideMask(current_mask | model_collision_mask)
|
||||
print(f"为 {model.getName()} 启用模型间碰撞检测")
|
||||
|
||||
# 获取模型的边界
|
||||
bounds = model.getBounds()
|
||||
if bounds.isEmpty():
|
||||
print(f"⚠️ 模型 {model.getName()} 边界为空,使用默认碰撞体")
|
||||
# 使用默认的小球体
|
||||
cSphere = CollisionSphere(Point3(0, 0, 0), 1.0)
|
||||
else:
|
||||
center = bounds.getCenter()
|
||||
radius = bounds.getRadius()
|
||||
|
||||
# 添加碰撞球体
|
||||
cSphere = CollisionSphere(center, radius)
|
||||
# 确保半径不为零
|
||||
if radius <= 0:
|
||||
radius = 1.0
|
||||
print(f"⚠️ 模型 {model.getName()} 半径为零,使用默认半径 1.0")
|
||||
#
|
||||
# # 添加碰撞球体
|
||||
# cSphere = CollisionSphere(center, radius)
|
||||
cSphere = self.world.collision_manager.createCollisionShape(model, 'polygon')
|
||||
|
||||
cNode.addSolid(cSphere)
|
||||
|
||||
# 将碰撞节点附加到模型上
|
||||
cNodePath = model.attachNewNode(cNode)
|
||||
#cNodePath.hide()
|
||||
# cNodePath.show() # 取消注释可以显示碰撞体,用于调试
|
||||
|
||||
# 根据调试设置决定是否显示碰撞体
|
||||
if hasattr(self.world, 'debug_collision') and self.world.debug_collision:
|
||||
cNodePath.show()
|
||||
else:
|
||||
cNodePath.hide()
|
||||
|
||||
# 为模型添加碰撞相关标签
|
||||
model.setTag("has_collision", "true")
|
||||
model.setTag("collision_radius", str(radius if 'radius' in locals() else 1.0))
|
||||
|
||||
print(f"✅ 为模型 {model.getName()} 设置碰撞检测完成")
|
||||
|
||||
return cNodePath
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 为模型 {model.getName()} 设置碰撞检测失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
# ==================== 场景树管理 ====================
|
||||
|
||||
@ -890,99 +959,489 @@ class SceneManager:
|
||||
print(f"异步加载模型完成: {model.getName()}")
|
||||
|
||||
def createSpotLight(self, pos=(0, 0, 0)):
|
||||
from RenderPipelineFile.rpcore import SpotLight, RenderPipeline
|
||||
from panda3d.core import Vec3,NodePath
|
||||
"""创建聚光灯 - 支持多选创建,优化版本"""
|
||||
try:
|
||||
from RenderPipelineFile.rpcore import SpotLight
|
||||
from QPanda3D.Panda3DWorld import get_render_pipeline
|
||||
from panda3d.core import Vec3, NodePath
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
print(f"🔆 开始创建聚光灯,位置: {pos}")
|
||||
|
||||
# 获取树形控件
|
||||
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_lights = []
|
||||
render_pipeline = get_render_pipeline()
|
||||
|
||||
# 创建一个挂载节点(你控制的)
|
||||
light_np = NodePath("SpotlightAttachNode")
|
||||
light_np.reparentTo(self.world.render)
|
||||
#light_np.setPos(*pos)
|
||||
|
||||
self.half_energy = 5000
|
||||
self.lamp_fov = 70
|
||||
self.lamp_radius = 1000
|
||||
|
||||
light = SpotLight()
|
||||
light.direction = Vec3(0, 0, -1) # 光照方向
|
||||
light.fov = self.lamp_fov # 光源角度(类似手电筒)
|
||||
light.set_color_from_temperature(5 * 1000.0) # 色温(K)
|
||||
light.energy = self.half_energy # 光照强度
|
||||
light.radius = self.lamp_radius # 影响范围
|
||||
light.casts_shadows = True # 是否投射阴影
|
||||
light.shadow_map_resolution = 256 # 阴影分辨率
|
||||
light.setPos(*pos)
|
||||
#light_np.setPos(*pos)
|
||||
|
||||
#light_np = render_pipeline.add_light(light, parent=self.world.render)
|
||||
render_pipeline.add_light(light) # 添加到渲染管线
|
||||
|
||||
# 为每个有效的父节点创建聚光灯
|
||||
for parent_item, parent_node in target_parents:
|
||||
try:
|
||||
# 生成唯一名称
|
||||
light_name = f"Spotlight_{len(self.Spotlight)}"
|
||||
|
||||
light_np.setName(light_name) # 设置唯一名称
|
||||
#light_np.reparentTo(self.world.render) # 挂载到场景根节点
|
||||
# 创建挂载节点 - 挂载到选中的父节点
|
||||
light_np = NodePath(light_name)
|
||||
light_np.reparentTo(parent_node) # 挂载到父节点而不是render
|
||||
light_np.setPos(*pos)
|
||||
|
||||
# 创建聚光灯对象
|
||||
light = SpotLight()
|
||||
light.direction = Vec3(0, 0, -1)
|
||||
light.fov = 70
|
||||
light.set_color_from_temperature(5 * 1000.0)
|
||||
light.energy = 5000
|
||||
light.radius = 1000
|
||||
light.casts_shadows = True
|
||||
light.shadow_map_resolution = 256
|
||||
|
||||
# 设置光源的世界坐标位置
|
||||
world_pos = light_np.getPos(self.world.render)
|
||||
light.setPos(world_pos)
|
||||
|
||||
# 添加到渲染管线
|
||||
render_pipeline.add_light(light)
|
||||
|
||||
# 设置节点属性和标签
|
||||
light_np.setTag("light_type", "spot_light")
|
||||
light_np.setTag("is_scene_element", "1")
|
||||
light_np.setTag("light_energy", str(light.energy))
|
||||
light_np.setTag("created_by_user", "1")
|
||||
|
||||
# 保存光源对象引用
|
||||
light_np.setPythonTag("rp_light_object", light)
|
||||
|
||||
# 添加到管理列表
|
||||
self.Spotlight.append(light_np)
|
||||
|
||||
if hasattr(self.world, 'updateSceneTree'):
|
||||
self.world.updateSceneTree()
|
||||
print(f"✅ 为 {parent_item.text(0)} 创建聚光灯成功: {light_name}")
|
||||
|
||||
#print("nikan"+light_np.getHpr())
|
||||
# 在Qt树形控件中添加对应节点
|
||||
qt_item = tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE")
|
||||
if qt_item:
|
||||
created_lights.append((light_np, qt_item))
|
||||
else:
|
||||
created_lights.append((light_np, None))
|
||||
print("⚠️ Qt树节点添加失败,但Panda3D对象已创建")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 为 {parent_item.text(0)} 创建聚光灯失败: {str(e)}")
|
||||
continue
|
||||
|
||||
# 处理创建结果
|
||||
if not created_lights:
|
||||
print("❌ 没有成功创建任何聚光灯")
|
||||
return None
|
||||
|
||||
# 选中最后创建的光源
|
||||
if created_lights:
|
||||
last_light_np, last_qt_item = created_lights[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_lights)} 个聚光灯")
|
||||
|
||||
# 返回值处理
|
||||
if len(created_lights) == 1:
|
||||
return created_lights[0][0] # 单个光源返回NodePath
|
||||
else:
|
||||
return [light_np for light_np, _ in created_lights] # 多个光源返回列表
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 创建聚光灯过程失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def createPointLight(self, pos=(0, 0, 0)):
|
||||
from RenderPipelineFile.rpcore import PointLight, RenderPipeline
|
||||
"""创建点光源 - 支持多选创建,优化版本"""
|
||||
try:
|
||||
from RenderPipelineFile.rpcore import PointLight
|
||||
from QPanda3D.Panda3DWorld import get_render_pipeline
|
||||
from panda3d.core import Vec3, NodePath
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
print(f"💡 开始创建点光源,位置: {pos}")
|
||||
|
||||
# 获取树形控件
|
||||
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_lights = []
|
||||
render_pipeline = get_render_pipeline()
|
||||
|
||||
# 创建一个挂载节点(你控制的)
|
||||
light_np = NodePath("PointlightAttachNode")
|
||||
light_np.reparentTo(self.world.render)
|
||||
# 为每个有效的父节点创建点光源
|
||||
for parent_item, parent_node in target_parents:
|
||||
try:
|
||||
# 生成唯一名称
|
||||
light_name = f"Pointlight_{len(self.Pointlight)}"
|
||||
|
||||
|
||||
light = PointLight()
|
||||
light.setPos(*pos)
|
||||
# 创建挂载节点 - 挂载到选中的父节点
|
||||
light_np = NodePath(light_name)
|
||||
light_np.reparentTo(parent_node) # 挂载到父节点而不是render
|
||||
light_np.setPos(*pos)
|
||||
|
||||
# 创建点光源对象
|
||||
light = PointLight()
|
||||
|
||||
# 设置光源的世界坐标位置
|
||||
world_pos = light_np.getPos(self.world.render)
|
||||
light.setPos(world_pos)
|
||||
|
||||
light.energy = 5000
|
||||
light.radius = 1000
|
||||
light.inner_radius = 0.4
|
||||
light.set_color_from_temperature(5 * 1000.0) # 色温(K)
|
||||
light.casts_shadows = True # 是否投射阴影
|
||||
light.shadow_map_resolution = 256 # 阴影分辨率
|
||||
light.set_color_from_temperature(5 * 1000.0)
|
||||
light.casts_shadows = True
|
||||
light.shadow_map_resolution = 256
|
||||
|
||||
render_pipeline.add_light(light) # 添加到渲染管线
|
||||
|
||||
light_name = f"Pointlight{len(self.Pointlight)}"
|
||||
|
||||
light_np.setName(light_name) # 设置唯一名称
|
||||
|
||||
#light_np = NodePath(f"PointLight_{len(self.Pointlight)}")
|
||||
#light_np.reparentTo(self.world.render)
|
||||
#light_np.setPos(*pos)
|
||||
# 添加到渲染管线
|
||||
render_pipeline.add_light(light)
|
||||
|
||||
# 设置节点属性和标签
|
||||
light_np.setTag("light_type", "point_light")
|
||||
light_np.setTag("is_scene_element", "1")
|
||||
light_np.setTag("light_energy", str(light.energy))
|
||||
light_np.setTag("created_by_user", "1")
|
||||
|
||||
# 保存光源对象引用(重要!用于属性面板)
|
||||
# 保存光源对象引用
|
||||
light_np.setPythonTag("rp_light_object", light)
|
||||
|
||||
# 添加到管理列表
|
||||
self.Pointlight.append(light_np)
|
||||
|
||||
if hasattr(self.world, 'updateSceneTree'):
|
||||
self.world.updateSceneTree()
|
||||
print(f"✅ 为 {parent_item.text(0)} 创建点光源成功: {light_name}")
|
||||
|
||||
return light,light_np
|
||||
# 在Qt树形控件中添加对应节点
|
||||
qt_item =tree_widget.add_node_to_tree_widget(light_np, parent_item, "LIGHT_NODE")
|
||||
if qt_item:
|
||||
created_lights.append((light_np, qt_item))
|
||||
else:
|
||||
created_lights.append((light_np, None))
|
||||
print("⚠️ Qt树节点添加失败,但Panda3D对象已创建")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 为 {parent_item.text(0)} 创建点光源失败: {str(e)}")
|
||||
continue
|
||||
|
||||
# 处理创建结果
|
||||
if not created_lights:
|
||||
print("❌ 没有成功创建任何点光源")
|
||||
return None
|
||||
|
||||
# 选中最后创建的光源
|
||||
if created_lights:
|
||||
last_light_np, last_qt_item = created_lights[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(last_light_np, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_lights)} 个点光源")
|
||||
|
||||
# 返回值处理
|
||||
if len(created_lights) == 1:
|
||||
return created_lights[0][0] # 单个光源返回NodePath
|
||||
else:
|
||||
return [light_np for light_np, _ in created_lights] # 多个光源返回列表
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 创建点光源过程失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _get_tree_widget(self):
|
||||
"""安全获取树形控件"""
|
||||
try:
|
||||
if (hasattr(self.world, 'interface_manager') and
|
||||
hasattr(self.world.interface_manager, 'treeWidget')):
|
||||
return self.world.interface_manager.treeWidget
|
||||
except AttributeError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _importModelSingle(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
|
||||
"""传统单一模型导入方法(兼容性保留)"""
|
||||
try:
|
||||
print(f"\n=== 使用传统模式导入模型: {filepath} ===")
|
||||
|
||||
filepath = util.normalize_model_path(filepath)
|
||||
original_filepath = filepath
|
||||
|
||||
# 检查是否需要转换为GLB
|
||||
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
|
||||
print(f"🔄 检测到需要转换的格式,尝试转换为GLB...")
|
||||
converted_path = self._convertToGLBWithProgress(filepath)
|
||||
if converted_path:
|
||||
print(f"✅ 转换成功: {converted_path}")
|
||||
filepath = converted_path
|
||||
try:
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
original_ext = os.path.splitext(original_filepath)[1].upper()
|
||||
QMessageBox.information(None, "转换成功",
|
||||
f"已将 {original_ext} 格式自动转换为 GLB 格式\n以获得更好的动画支持!")
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
print(f"⚠️ 转换失败,使用原始文件")
|
||||
|
||||
# 加载模型
|
||||
print("直接从文件加载模型...")
|
||||
model = self.world.loader.loadModel(filepath)
|
||||
if not model:
|
||||
print("加载模型失败")
|
||||
return None
|
||||
|
||||
# 设置模型名称
|
||||
model_name = os.path.basename(filepath)
|
||||
model.setName(model_name)
|
||||
|
||||
# 将模型添加到场景
|
||||
model.reparentTo(self.world.render)
|
||||
|
||||
# 设置标签和路径信息
|
||||
model.setTag("model_path", filepath)
|
||||
model.setTag("original_path", original_filepath)
|
||||
model.setTag("file", model_name)
|
||||
model.setTag("is_model_root", "1")
|
||||
model.setTag("is_scene_element", "1")
|
||||
model.setTag("created_by_user", "1")
|
||||
|
||||
if filepath != original_filepath:
|
||||
model.setTag("converted_from", os.path.splitext(original_filepath)[1])
|
||||
model.setTag("converted_to_glb", "true")
|
||||
|
||||
# 应用处理选项
|
||||
if apply_unit_conversion and filepath.lower().endswith('.fbx'):
|
||||
print("应用FBX单位转换(厘米到米)...")
|
||||
self._applyUnitConversion(model, 0.01)
|
||||
model.setTag("unit_conversion_applied", "true")
|
||||
|
||||
if normalize_scales and filepath.lower().endswith('.fbx'):
|
||||
print("标准化FBX模型缩放层级...")
|
||||
self._normalizeModelScales(model)
|
||||
model.setTag("scale_normalization_applied", "true")
|
||||
|
||||
# 调整模型位置到地面
|
||||
self._adjustModelToGround(model)
|
||||
|
||||
# 创建并设置基础材质
|
||||
print("\n=== 开始设置材质 ===")
|
||||
self._applyMaterialsToModel(model)
|
||||
|
||||
# 设置碰撞检测(重要!用于选择功能)
|
||||
print("\n=== 设置碰撞检测 ===")
|
||||
self.setupCollision(model)
|
||||
|
||||
# 添加到模型列表
|
||||
self.models.append(model)
|
||||
|
||||
# 更新场景树
|
||||
self.updateSceneTree()
|
||||
|
||||
print(f"=== 模型导入成功: {model_name} ===\n")
|
||||
return model
|
||||
|
||||
except Exception as e:
|
||||
print(f"导入模型失败: {str(e)}")
|
||||
return None
|
||||
|
||||
# def createSpotLight(self, pos=(0, 0, 0)):
|
||||
# """创建聚光灯 - 使用统一的create_item方法"""
|
||||
# try:
|
||||
# # 调用CustomTreeWidget的create_item方法创建聚光灯节点
|
||||
# if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
|
||||
# tree_widget = self.world.interface_manager.treeWidget
|
||||
# if tree_widget and hasattr(tree_widget, 'create_item'):
|
||||
# # 创建聚光灯节点
|
||||
# created_nodes = tree_widget.create_item("spot_light")
|
||||
#
|
||||
# if created_nodes:
|
||||
# # 获取创建的节点
|
||||
# light_np, qt_item = created_nodes[0]
|
||||
#
|
||||
# # 设置位置(如果指定了非默认位置)
|
||||
# if pos != (0, 0, 0):
|
||||
# light_np.setPos(*pos)
|
||||
# # 同时更新光源对象的位置
|
||||
# light_obj = light_np.getPythonTag("rp_light_object")
|
||||
# if light_obj:
|
||||
# light_obj.setPos(*pos)
|
||||
#
|
||||
# print(f"✅ 通过create_item创建聚光灯成功: {light_np.getName()}")
|
||||
# return light_np
|
||||
# else:
|
||||
# print("❌ create_item创建聚光灯失败")
|
||||
# return None
|
||||
# else:
|
||||
# print("❌ 无法访问树形控件的create_item方法")
|
||||
# return None
|
||||
# else:
|
||||
# print("❌ 无法访问界面管理器或树形控件")
|
||||
# return None
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f"❌ 创建聚光灯时发生错误: {str(e)}")
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
# return None
|
||||
|
||||
# def createSpotLight(self, pos=(0, 0, 0)):
|
||||
# from RenderPipelineFile.rpcore import SpotLight, RenderPipeline
|
||||
# from panda3d.core import Vec3,NodePath
|
||||
#
|
||||
# render_pipeline = get_render_pipeline()
|
||||
#
|
||||
# # 创建一个挂载节点(你控制的)
|
||||
# light_np = NodePath("SpotlightAttachNode")
|
||||
# light_np.reparentTo(self.world.render)
|
||||
# #light_np.setPos(*pos)
|
||||
#
|
||||
# self.half_energy = 5000
|
||||
# self.lamp_fov = 70
|
||||
# self.lamp_radius = 1000
|
||||
#
|
||||
# light = SpotLight()
|
||||
# light.direction = Vec3(0, 0, -1) # 光照方向
|
||||
# light.fov = self.lamp_fov # 光源角度(类似手电筒)
|
||||
# light.set_color_from_temperature(5 * 1000.0) # 色温(K)
|
||||
# light.energy = self.half_energy # 光照强度
|
||||
# light.radius = self.lamp_radius # 影响范围
|
||||
# light.casts_shadows = True # 是否投射阴影
|
||||
# light.shadow_map_resolution = 256 # 阴影分辨率
|
||||
# light.setPos(*pos)
|
||||
# #light_np.setPos(*pos)
|
||||
#
|
||||
# #light_np = render_pipeline.add_light(light, parent=self.world.render)
|
||||
# render_pipeline.add_light(light) # 添加到渲染管线
|
||||
#
|
||||
# light_name = f"Spotlight_{len(self.Spotlight)}"
|
||||
#
|
||||
# light_np.setName(light_name) # 设置唯一名称
|
||||
# #light_np.reparentTo(self.world.render) # 挂载到场景根节点
|
||||
#
|
||||
# light_np.setTag("light_type", "spot_light")
|
||||
# light_np.setTag("is_scene_element", "1")
|
||||
# light_np.setTag("light_energy", str(light.energy))
|
||||
#
|
||||
# light_np.setPythonTag("rp_light_object", light)
|
||||
#
|
||||
# self.Spotlight.append(light_np)
|
||||
#
|
||||
# if hasattr(self.world, 'updateSceneTree'):
|
||||
# self.world.updateSceneTree()
|
||||
#
|
||||
# #print("nikan"+light_np.getHpr())
|
||||
|
||||
# def createPointLight(self, pos=(0, 0, 0)):
|
||||
# """创建点光源 - 使用统一的create_item方法"""
|
||||
# try:
|
||||
# # 调用CustomTreeWidget的create_item方法创建点光源节点
|
||||
# if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'):
|
||||
# tree_widget = self.world.interface_manager.treeWidget
|
||||
# if tree_widget and hasattr(tree_widget, 'create_item'):
|
||||
# # 创建点光源节点
|
||||
# created_nodes = tree_widget.create_item("point_light")
|
||||
#
|
||||
# if created_nodes:
|
||||
# # 获取创建的节点
|
||||
# light_np, qt_item = created_nodes[0]
|
||||
#
|
||||
# # 设置位置(如果指定了非默认位置)
|
||||
# if pos != (0, 0, 0):
|
||||
# light_np.setPos(*pos)
|
||||
# # 同时更新光源对象的位置
|
||||
# light_obj = light_np.getPythonTag("rp_light_object")
|
||||
# if light_obj:
|
||||
# light_obj.setPos(*pos)
|
||||
#
|
||||
# print(f"✅ 通过create_item创建点光源成功: {light_np.getName()}")
|
||||
# return light_np
|
||||
# else:
|
||||
# print("❌ create_item创建点光源失败")
|
||||
# return None
|
||||
# else:
|
||||
# print("❌ 无法访问树形控件的create_item方法")
|
||||
# return None
|
||||
# else:
|
||||
# print("❌ 无法访问界面管理器或树形控件")
|
||||
# return None
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f"❌ 创建点光源时发生错误: {str(e)}")
|
||||
# import traceback
|
||||
# traceback.print_exc()
|
||||
# return None
|
||||
|
||||
# def createPointLight(self, pos=(0, 0, 0)):
|
||||
# from RenderPipelineFile.rpcore import PointLight, RenderPipeline
|
||||
# from panda3d.core import Vec3, NodePath
|
||||
#
|
||||
# render_pipeline = get_render_pipeline()
|
||||
#
|
||||
# # 创建一个挂载节点(你控制的)
|
||||
# light_np = NodePath("PointlightAttachNode")
|
||||
# light_np.reparentTo(self.world.render)
|
||||
#
|
||||
#
|
||||
# light = PointLight()
|
||||
# light.setPos(*pos)
|
||||
# light_np.setPos(*pos)
|
||||
# light.energy = 5000
|
||||
# light.radius = 1000
|
||||
# light.inner_radius = 0.4
|
||||
# light.set_color_from_temperature(5 * 1000.0) # 色温(K)
|
||||
# light.casts_shadows = True # 是否投射阴影
|
||||
# light.shadow_map_resolution = 256 # 阴影分辨率
|
||||
#
|
||||
# render_pipeline.add_light(light) # 添加到渲染管线
|
||||
#
|
||||
# light_name = f"Pointlight{len(self.Pointlight)}"
|
||||
#
|
||||
# light_np.setName(light_name) # 设置唯一名称
|
||||
#
|
||||
# #light_np = NodePath(f"PointLight_{len(self.Pointlight)}")
|
||||
# #light_np.reparentTo(self.world.render)
|
||||
# #light_np.setPos(*pos)
|
||||
#
|
||||
# light_np.setTag("light_type", "point_light")
|
||||
# light_np.setTag("is_scene_element", "1")
|
||||
# light_np.setTag("light_energy", str(light.energy))
|
||||
#
|
||||
# # 保存光源对象引用(重要!用于属性面板)
|
||||
# light_np.setPythonTag("rp_light_object", light)
|
||||
#
|
||||
# self.Pointlight.append(light_np)
|
||||
#
|
||||
# if hasattr(self.world, 'updateSceneTree'):
|
||||
# self.world.updateSceneTree()
|
||||
#
|
||||
# return light,light_np
|
||||
|
||||
# ==================== GLB 转换方法 ====================
|
||||
|
||||
#===================================================
|
||||
|
||||
def _shouldConvertToGLB(self, filepath):
|
||||
"""判断是否应该转换为GLB格式"""
|
||||
ext = os.path.splitext(filepath)[1].lower()
|
||||
|
||||
@ -18,8 +18,8 @@ class InterfaceManager:
|
||||
self.treeWidget = treeWidget
|
||||
|
||||
# 添加右键菜单
|
||||
self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.treeWidget.customContextMenuRequested.connect(self.showTreeContextMenu)
|
||||
# self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
# self.treeWidget.customContextMenuRequested.connect(self.showTreeContextMenu)
|
||||
|
||||
# 更新场景树
|
||||
self.world.scene_manager.updateSceneTree()
|
||||
@ -155,10 +155,12 @@ class InterfaceManager:
|
||||
|
||||
# 创建场景根节点
|
||||
sceneRoot = QTreeWidgetItem(self.treeWidget, ['场景'])
|
||||
|
||||
sceneRoot.setData(0, Qt.UserRole, self.world.render)
|
||||
sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT")
|
||||
# 添加相机节点
|
||||
cameraItem = QTreeWidgetItem(sceneRoot, ['相机'])
|
||||
cameraItem.setData(0, Qt.UserRole, self.world.cam)
|
||||
cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE")
|
||||
print("添加相机节点")
|
||||
|
||||
# # 添加模型节点组
|
||||
@ -223,6 +225,7 @@ class InterfaceManager:
|
||||
if hasattr(self.world, 'ground') and self.world.ground:
|
||||
groundItem = QTreeWidgetItem(sceneRoot, ['地板'])
|
||||
groundItem.setData(0, Qt.UserRole, self.world.ground)
|
||||
groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE")
|
||||
|
||||
# 展开所有节点
|
||||
#self.treeWidget.expandAll()
|
||||
|
||||
@ -8,13 +8,15 @@
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from PyQt5.QtGui import QKeySequence, QIcon
|
||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction,
|
||||
QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem,
|
||||
QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea,
|
||||
QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout,
|
||||
QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget)
|
||||
from PyQt5.QtCore import Qt, QDir, QTimer
|
||||
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget
|
||||
QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QFrame)
|
||||
from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint
|
||||
from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget, CustomAssetsTreeWidget, CustomConsoleDockWidget
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""主窗口类"""
|
||||
@ -22,10 +24,12 @@ 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()
|
||||
|
||||
# 移动窗口到屏幕中央
|
||||
@ -44,6 +48,9 @@ class MainWindow(QMainWindow):
|
||||
self.pandaWidget = CustomPanda3DWidget(self.world)
|
||||
self.setCentralWidget(self.pandaWidget)
|
||||
|
||||
# 创建内嵌工具栏
|
||||
self.setupEmbeddedToolbar()
|
||||
|
||||
def move_center(self):
|
||||
"""设置窗口居中显示"""
|
||||
self.setGeometry(50, 50, 1920, 1080)
|
||||
@ -54,6 +61,204 @@ 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()
|
||||
@ -93,39 +298,19 @@ class MainWindow(QMainWindow):
|
||||
self.sunsetAction = self.toolsMenu.addAction('光照编辑')
|
||||
self.pluginAction = self.toolsMenu.addAction('图形编辑')
|
||||
|
||||
# 创建菜单
|
||||
# 统一创建菜单 - 关键修改
|
||||
self.createMenu = menubar.addMenu('创建')
|
||||
self.createEnptyaddAction = self.createMenu.addAction('空对象')
|
||||
self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象')
|
||||
self.setupCreateMenuActions() # 统一创建菜单动作
|
||||
|
||||
self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
|
||||
self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
|
||||
|
||||
self.createGUIaddMenu = self.createMenu.addMenu('GUI')
|
||||
self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
|
||||
self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
|
||||
self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
|
||||
self.createGUIaddMenu.addSeparator()
|
||||
self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
|
||||
|
||||
self.createLightaddMenu = self.createMenu.addMenu('光源')
|
||||
self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
|
||||
self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
|
||||
|
||||
# GUI菜单
|
||||
# GUI菜单 - 复用创建菜单的动作
|
||||
self.guiMenu = menubar.addMenu('GUI')
|
||||
self.guiEditModeAction = self.guiMenu.addAction('进入GUI编辑模式')
|
||||
self.guiMenu.addSeparator()
|
||||
# self.createButtonAction = self.guiMenu.addAction('创建按钮')
|
||||
# self.createLabelAction = self.guiMenu.addAction('创建标签')
|
||||
# self.createEntryAction = self.guiMenu.addAction('创建输入框')
|
||||
self.guiMenu.addAction(self.createButtonAction)
|
||||
self.guiMenu.addAction(self.createLabelAction)
|
||||
self.guiMenu.addAction(self.createEntryAction)
|
||||
self.guiMenu.addSeparator()
|
||||
# self.create3DTextAction = self.guiMenu.addAction('创建3D文本')
|
||||
self.guiMenu.addAction(self.create3DTextAction)
|
||||
# self.createVirtualScreenAction = self.guiMenu.addAction('创建虚拟屏幕')
|
||||
self.guiMenu.addAction(self.createVirtualScreenAction)
|
||||
|
||||
# 脚本菜单
|
||||
@ -144,32 +329,95 @@ class MainWindow(QMainWindow):
|
||||
self.helpMenu = menubar.addMenu('帮助')
|
||||
self.aboutAction = self.helpMenu.addAction('关于')
|
||||
|
||||
def setupCreateMenuActions(self):
|
||||
"""统一设置创建菜单的所有动作 - 避免重复代码"""
|
||||
# 基础对象
|
||||
self.createEnptyaddAction = self.createMenu.addAction('空对象')
|
||||
|
||||
# 3D对象子菜单
|
||||
self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象')
|
||||
# 可以在这里添加更多3D对象类型
|
||||
|
||||
# 3D GUI子菜单
|
||||
self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI')
|
||||
self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本')
|
||||
|
||||
# GUI子菜单
|
||||
self.createGUIaddMenu = self.createMenu.addMenu('GUI')
|
||||
self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮')
|
||||
self.createLabelAction = self.createGUIaddMenu.addAction('创建标签')
|
||||
self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框')
|
||||
self.createGUIaddMenu.addSeparator()
|
||||
self.createVideoScreen = self.createGUIaddMenu.addAction('创建视频屏幕')
|
||||
self.createSphericalVideo = self.createGUIaddMenu.addAction('创建球形视频')
|
||||
self.createGUIaddMenu.addSeparator()
|
||||
self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕')
|
||||
|
||||
# 光源子菜单
|
||||
self.createLightaddMenu = self.createMenu.addMenu('光源')
|
||||
self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯')
|
||||
self.createPointLightAction = self.createLightaddMenu.addAction('点光源')
|
||||
|
||||
# 统一连接信号到处理方法
|
||||
self.connectCreateMenuActions()
|
||||
|
||||
def connectCreateMenuActions(self):
|
||||
"""统一连接创建菜单的信号到处理方法"""
|
||||
# 连接到world对象的创建方法
|
||||
# self.createEnptyaddAction.triggered.connect(self.world.createEmptyObject)
|
||||
self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
|
||||
self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
|
||||
self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
|
||||
self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
|
||||
# self.createVideoScreen.triggered.connect(self.world.createVideoScreen)
|
||||
# self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo)
|
||||
# self.createVirtualScreenAction.triggered.connect(self.world.createVirtualScreen)
|
||||
self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
|
||||
self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
|
||||
|
||||
# # self.createVideoScreen.triggered.connect(lambda: self.world.video_manager.create_video_screen(
|
||||
# # pos=(0, 0, 2),
|
||||
# # size=(4, 3),
|
||||
# # name=f"video_screen_{len(self.world.video_manager.video_objects) if hasattr(self.world, 'video_manager') else 0}"
|
||||
# # ))
|
||||
# # self.createSphericalVideo.triggered.connect(lambda: self.world.createGUISphericalVideo())
|
||||
# self.createVirtualScreenAction.triggered.connect(lambda: self.world.create_spherical_video())
|
||||
|
||||
def getCreateMenuActions(self):
|
||||
"""获取所有创建菜单动作的字典 - 供右键菜单使用"""
|
||||
return {
|
||||
'createEmpty': self.createEnptyaddAction,
|
||||
'create3DText': self.create3DTextAction,
|
||||
'createButton': self.createButtonAction,
|
||||
'createLabel': self.createLabelAction,
|
||||
'createEntry': self.createEntryAction,
|
||||
'createVideoScreen': self.createVideoScreen,
|
||||
'createSphericalVideo': self.createSphericalVideo,
|
||||
'createVirtualScreen': self.createVirtualScreenAction,
|
||||
'createSpotLight': self.createSpotLightAction,
|
||||
'createPointLight': self.createPointLightAction,
|
||||
}
|
||||
|
||||
def setupDockWindows(self):
|
||||
"""创建停靠窗口"""
|
||||
# 创建左侧停靠窗口(层级窗口)
|
||||
self.leftDock = QDockWidget("层级", self)
|
||||
self.leftDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
|
||||
self.treeWidget = CustomTreeWidget(self.world)
|
||||
self.world.setTreeWidget(self.treeWidget) # 设置树形控件引用
|
||||
self.leftDock.setWidget(self.treeWidget)
|
||||
# self.leftDock.setMinimumWidth(300)
|
||||
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock)
|
||||
|
||||
|
||||
# 创建右侧停靠窗口(属性窗口)
|
||||
self.rightDock = QDockWidget("属性", self)
|
||||
self.rightDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
|
||||
|
||||
# 创建属性面板的主容器和布局
|
||||
self.propertyContainer = QWidget()
|
||||
self.propertyContainer.setObjectName("PropertyContainer")
|
||||
self.propertyLayout = QVBoxLayout(self.propertyContainer)
|
||||
# self.propertyLayout = QFormLayout(self.propertyContainer)
|
||||
|
||||
# 添加初始提示信息
|
||||
tipLabel = QLabel("")
|
||||
tipLabel.setStyleSheet("color: gray;") # 使用灰色字体
|
||||
# self.propertyLayout.addRow(tipLabel)
|
||||
self.propertyLayout.addWidget(tipLabel)
|
||||
|
||||
# 创建滚动区域并设置属性
|
||||
@ -185,41 +433,69 @@ 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('工具栏')
|
||||
@ -276,12 +552,8 @@ 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()
|
||||
@ -389,9 +661,6 @@ class MainWindow(QMainWindow):
|
||||
# 添加拉伸以填充剩余空间
|
||||
layout.addStretch()
|
||||
|
||||
# 设置到停靠窗口
|
||||
self.scriptDock.setWidget(scriptContainer)
|
||||
|
||||
# 初始化脚本列表
|
||||
self.refreshScriptsList()
|
||||
|
||||
@ -414,24 +683,32 @@ class MainWindow(QMainWindow):
|
||||
# 连接GUI编辑模式事件
|
||||
self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode())
|
||||
|
||||
# 连接创建事件
|
||||
# 连接光源创建按钮事件
|
||||
self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
|
||||
self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
|
||||
# 连接GUI创建按钮事件
|
||||
self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
|
||||
self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
|
||||
self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
|
||||
self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
|
||||
#self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
|
||||
self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
|
||||
|
||||
# 连接工具栏GUI创建按钮事件
|
||||
self.createButtonTool.clicked.connect(lambda: self.world.createGUIButton())
|
||||
self.createLabelTool.clicked.connect(lambda: self.world.createGUILabel())
|
||||
self.create3DTextTool.clicked.connect(lambda: self.world.createGUI3DText())
|
||||
self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight())
|
||||
self.createPointLight.clicked.connect(lambda :self.world.createPointLight())
|
||||
# # 连接创建事件
|
||||
# # 连接光源创建按钮事件
|
||||
# self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
|
||||
# self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
|
||||
# # 连接GUI创建按钮事件
|
||||
# self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton())
|
||||
# self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel())
|
||||
# self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry())
|
||||
# self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText())
|
||||
# #self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
|
||||
#
|
||||
# # self.createVideoScreen.triggered.connect(lambda: self.world.video_manager.create_video_screen(
|
||||
# # pos=(0, 0, 2),
|
||||
# # size=(4, 3),
|
||||
# # name=f"video_screen_{len(self.world.video_manager.video_objects) if hasattr(self.world, 'video_manager') else 0}"
|
||||
# # ))
|
||||
# # self.createSphericalVideo.triggered.connect(lambda: self.world.createGUISphericalVideo())
|
||||
#
|
||||
# self.createVirtualScreenAction.triggered.connect(lambda: self.world.create_spherical_video())
|
||||
#
|
||||
# # 连接工具栏GUI创建按钮事件
|
||||
# self.createButtonTool.clicked.connect(lambda: self.world.createGUIButton())
|
||||
# self.createLabelTool.clicked.connect(lambda: self.world.createGUILabel())
|
||||
# self.create3DTextTool.clicked.connect(lambda: self.world.createGUI3DText())
|
||||
# self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight())
|
||||
# self.createPointLight.clicked.connect(lambda :self.world.createPointLight())
|
||||
|
||||
# 连接树节点点击信号
|
||||
# self.treeWidget.itemClicked.connect(self.world.onTreeItemClicked)
|
||||
|
||||
@ -504,6 +504,7 @@ class PropertyPanelManager:
|
||||
success = self.world.gui_manager.editGUIElement(gui_element, "text", text)
|
||||
if success:
|
||||
# 更新场景树显示的名称
|
||||
pass #CH
|
||||
self.world.scene_manager.updateSceneTree()
|
||||
|
||||
textEdit.textChanged.connect(updateText)
|
||||
|
||||