视频屏幕可播放视频流,修改部分名称过长问题
This commit is contained in:
parent
5e68ff5ee6
commit
4ca5068b2c
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -157,17 +157,23 @@ class GUIManager:
|
||||
# 父节点是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()}")
|
||||
parent_scale=parent_node.getScale()
|
||||
relative_scale = (
|
||||
size/parent_scale[0] if parent_scale[0]!=0 else size,
|
||||
size/parent_scale[1] if parent_scale[1]!=0 else size,
|
||||
size/parent_scale[2] if parent_scale[2]!=0 else size
|
||||
)
|
||||
else:
|
||||
# 父节点是普通3D节点 - 使用屏幕坐标
|
||||
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
|
||||
parent_gui_node = None # 使用默认的aspect2d
|
||||
relative_scale = size
|
||||
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
|
||||
|
||||
button = DirectButton(
|
||||
text=text,
|
||||
pos=gui_pos,
|
||||
scale=size,
|
||||
scale=relative_scale,
|
||||
command=self.onGUIButtonClick,
|
||||
extraArgs=[f"button_{len(self.gui_elements)}"],
|
||||
frameColor=(0.2, 0.6, 0.8, 1),
|
||||
@ -268,17 +274,22 @@ class GUIManager:
|
||||
# 父节点是GUI元素 - 作为子GUI挂载
|
||||
gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node)
|
||||
parent_gui_node = parent_node
|
||||
print(f"📎 挂载到GUI父节点: {parent_node.getName()}")
|
||||
|
||||
parent_scale = parent_node.getScale()
|
||||
relative_scale = (
|
||||
size/parent_scale[0] if parent_scale[0]!= 0 else size,
|
||||
size/parent_scale[1] if parent_scale[1]!= 0 else size,
|
||||
size/parent_scale[2] if parent_scale[2]!= 0 else size
|
||||
)
|
||||
else:
|
||||
# 父节点是普通3D节点 - 使用屏幕坐标
|
||||
gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1)
|
||||
parent_gui_node = None
|
||||
print(f"📎 挂载到3D父节点: {parent_item.text(0)}")
|
||||
|
||||
relative_scale = size
|
||||
label = DirectLabel(
|
||||
text=text,
|
||||
pos=gui_pos,
|
||||
scale=size,
|
||||
scale=relative_scale,
|
||||
frameColor=(0, 0, 0, 0), # 透明背景
|
||||
text_fg=(1, 1, 1, 1),
|
||||
text_font=self.world.getChineseFont() if self.world.getChineseFont() else None,
|
||||
@ -862,10 +873,9 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
def createVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None):
|
||||
"""创建视频播放屏幕 - 改进版本"""
|
||||
"""创建3D视频播放屏幕 - 添加占位符纹理支持"""
|
||||
try:
|
||||
from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage
|
||||
from PyQt5.QtCore import Qt
|
||||
import os
|
||||
|
||||
# 确保 pos 是有效的三维坐标元组
|
||||
@ -896,7 +906,7 @@ class GUIManager:
|
||||
return None
|
||||
|
||||
# 获取目标父节点列表
|
||||
target_parents = tree_widget.get_target_parents_for_gui_creation()
|
||||
target_parents = tree_widget.get_target_parents_for_creation()
|
||||
if not target_parents:
|
||||
print("❌ 没有找到有效的父节点")
|
||||
return None
|
||||
@ -925,6 +935,40 @@ class GUIManager:
|
||||
# 设置初始颜色为白色,确保纹理能正确显示
|
||||
video_screen.setColor(1, 1, 1, 1)
|
||||
|
||||
# 确保视频屏幕有正确的材质
|
||||
self._ensureVideoScreenMaterial(video_screen)
|
||||
|
||||
# 设置节点标签
|
||||
video_screen.setTag("gui_type", "video_screen")
|
||||
video_screen.setTag("gui_id", f"video_screen_{len(self.gui_elements)}")
|
||||
video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}")
|
||||
video_screen.setTag("is_gui_element", "1")
|
||||
video_screen.setTag("is_scene_element", "1")
|
||||
video_screen.setTag("created_by_user", "1")
|
||||
|
||||
# 设置视频路径标签
|
||||
if video_path and os.path.exists(video_path):
|
||||
video_screen.setTag("video_path", video_path)
|
||||
else:
|
||||
video_screen.setTag("video_path", "")
|
||||
|
||||
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
|
||||
placeholder_texture = Texture(f"placeholder_video_texture_3d_{len(self.gui_elements)}")
|
||||
placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb)
|
||||
placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76)
|
||||
placeholder_texture.setRamImage(placeholder_data)
|
||||
|
||||
# 创建纹理阶段并应用占位符纹理到视频屏幕
|
||||
texture_stage = TextureStage("video_placeholder")
|
||||
texture_stage.setSort(0)
|
||||
texture_stage.setMode(TextureStage.MModulate)
|
||||
video_screen.setTexture(texture_stage, placeholder_texture)
|
||||
|
||||
# 保存占位符纹理引用
|
||||
video_screen.setPythonTag("placeholder_texture", placeholder_texture)
|
||||
|
||||
print(f"🔧 为3D视频屏幕创建了占位符纹理环境: {screen_name}")
|
||||
|
||||
# 如果提供了视频路径,则加载视频纹理
|
||||
movie_texture = None
|
||||
if video_path and os.path.exists(video_path):
|
||||
@ -941,6 +985,9 @@ class GUIManager:
|
||||
|
||||
print(f"✅ 视频纹理加载成功: {video_path}")
|
||||
|
||||
# 保存视频纹理引用以便后续控制
|
||||
video_screen.setPythonTag("movie_texture", movie_texture)
|
||||
|
||||
# 尝试自动播放视频(如果支持)
|
||||
try:
|
||||
if hasattr(movie_texture, 'play'):
|
||||
@ -966,19 +1013,6 @@ class GUIManager:
|
||||
else:
|
||||
print("ℹ️ 未提供视频文件,显示默认占位符")
|
||||
|
||||
# 确保视频屏幕有正确的材质
|
||||
self._ensureVideoScreenMaterial(video_screen)
|
||||
|
||||
# 设置节点标签
|
||||
video_screen.setTag("gui_type", "video_screen")
|
||||
video_screen.setTag("gui_id", f"video_screen_{len(self.gui_elements)}")
|
||||
video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}")
|
||||
video_screen.setTag("is_gui_element", "1")
|
||||
video_screen.setTag("is_scene_element", "1")
|
||||
video_screen.setTag("created_by_user", "1")
|
||||
if video_path and os.path.exists(video_path):
|
||||
video_screen.setTag("video_path", video_path)
|
||||
|
||||
# 保存视频纹理引用以便后续控制
|
||||
if movie_texture:
|
||||
video_screen.setPythonTag("movie_texture", movie_texture)
|
||||
@ -1201,8 +1235,6 @@ class GUIManager:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def setVideoTime(self, video_screen, time_seconds):
|
||||
"""设置视频播放时间"""
|
||||
try:
|
||||
@ -1434,8 +1466,25 @@ class GUIManager:
|
||||
video_screen.setTag("is_scene_element", "1")
|
||||
video_screen.setTag("created_by_user", "1")
|
||||
|
||||
# 设置视频路径标签
|
||||
if video_path and os.path.exists(video_path):
|
||||
video_screen.setTag("video_path", video_path)
|
||||
else:
|
||||
video_screen.setTag("video_path", "")
|
||||
|
||||
# 关键修改:预先创建一个占位符纹理,为后续视频播放做准备
|
||||
placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}")
|
||||
placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb)
|
||||
placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76)
|
||||
placeholder_texture.setRamImage(placeholder_data)
|
||||
|
||||
# 应用占位符纹理到视频屏幕
|
||||
video_screen["frameTexture"] = placeholder_texture
|
||||
|
||||
# 保存占位符纹理引用
|
||||
video_screen.setPythonTag("placeholder_texture", placeholder_texture)
|
||||
|
||||
print(f"🔧 为2D视频屏幕创建了占位符纹理环境: {screen_name}")
|
||||
|
||||
# 如果提供了视频路径,则加载视频纹理
|
||||
movie_texture = None
|
||||
@ -1445,7 +1494,7 @@ class GUIManager:
|
||||
# 加载视频纹理
|
||||
movie_texture = self._loadMovieTexture(video_path)
|
||||
if movie_texture:
|
||||
# 应用纹理到视频屏幕
|
||||
# 应用纹理到视频屏幕(替换占位符)
|
||||
video_screen["frameTexture"] = movie_texture
|
||||
print(f"✅ 2D视频纹理加载成功: {video_path}")
|
||||
|
||||
@ -1546,143 +1595,349 @@ class GUIManager:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _loadMovieTexture(self, video_path):
|
||||
"""加载视频纹理的兼容方法"""
|
||||
try:
|
||||
from panda3d.core import Texture, MovieTexture
|
||||
import os
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(video_path):
|
||||
print(f"❌ 视频文件不存在: {video_path}")
|
||||
return None
|
||||
|
||||
print(f"🔍 尝试加载视频文件: {video_path}")
|
||||
|
||||
# 方法1: 尝试使用 MovieTexture(专门用于视频)
|
||||
try:
|
||||
movie_texture = MovieTexture(video_path)
|
||||
if movie_texture.read(video_path):
|
||||
print("✅ 使用 MovieTexture 成功加载视频")
|
||||
self._configureVideoTexture(movie_texture)
|
||||
return movie_texture
|
||||
else:
|
||||
print("⚠️ MovieTexture.read() 返回失败")
|
||||
except Exception as e:
|
||||
print(f"⚠️ MovieTexture 方法失败: {e}")
|
||||
|
||||
# 方法2: 尝试使用 loader.loadTexture
|
||||
try:
|
||||
movie_texture = self.world.loader.loadTexture(video_path)
|
||||
if movie_texture and hasattr(movie_texture, 'is_playable') and movie_texture.is_playable():
|
||||
print("✅ 使用 loader.loadTexture 成功加载视频纹理")
|
||||
self._configureVideoTexture(movie_texture)
|
||||
return movie_texture
|
||||
else:
|
||||
print("⚠️ loader.loadTexture 加载的不是可播放的视频纹理")
|
||||
except Exception as e:
|
||||
print(f"⚠️ loader.loadTexture 方法失败: {e}")
|
||||
|
||||
# 方法3: 尝试使用 Texture.read(作为最后备选)
|
||||
try:
|
||||
texture = Texture()
|
||||
if texture.read(video_path):
|
||||
print("✅ 使用 Texture.read 成功加载(可能作为静态纹理)")
|
||||
self._configureVideoTexture(texture)
|
||||
return texture
|
||||
except Exception as e:
|
||||
print(f"⚠️ Texture.read 方法失败: {e}")
|
||||
|
||||
print("❌ 所有视频纹理加载方法都失败")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 加载视频纹理时发生未知错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def play2DVideo(self, video_screen):
|
||||
"""播放2D视频"""
|
||||
try:
|
||||
# 获取视频纹理
|
||||
movie_texture = video_screen.getPythonTag("movie_texture")
|
||||
|
||||
# 备用获取方法
|
||||
if not movie_texture:
|
||||
# 尝试从frameTexture获取
|
||||
if hasattr(video_screen, 'frameTexture'):
|
||||
movie_texture = video_screen.frameTexture
|
||||
|
||||
if movie_texture and hasattr(movie_texture, 'play'):
|
||||
try:
|
||||
movie_texture.play()
|
||||
print(f"▶️ 继续播放2D视频: {video_screen.getName()}")
|
||||
if video_screen.hasPythonTag("video_capture"):
|
||||
print("视频已在播放中")
|
||||
return True
|
||||
except Exception as play_error:
|
||||
print(f"⚠️ 播放2D视频时出错: {play_error}")
|
||||
return False
|
||||
|
||||
# 获取视频路径并重新开始播放
|
||||
video_path = video_screen.getTag("video_path")
|
||||
if video_path:
|
||||
return self.world.property_manager._loadVideoFromURL(video_screen, video_path)
|
||||
else:
|
||||
print(f"⚠️ 2D视频屏幕没有可播放的视频纹理: {video_screen.getName()}")
|
||||
print("❌ 没有找到视频源")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 播放2D视频失败: {e}")
|
||||
print(f"❌ 播放视频失败: {e}")
|
||||
return False
|
||||
|
||||
def pause2DVideo(self, video_screen):
|
||||
"""暂停2D视频"""
|
||||
try:
|
||||
movie_texture = video_screen.getPythonTag("movie_texture")
|
||||
|
||||
# 备用获取方法
|
||||
if not movie_texture:
|
||||
# 尝试从frameTexture获取
|
||||
if hasattr(video_screen, 'frameTexture'):
|
||||
movie_texture = video_screen.frameTexture
|
||||
|
||||
if movie_texture and hasattr(movie_texture, 'stop'): # MovieTexture使用stop来暂停
|
||||
try:
|
||||
movie_texture.stop()
|
||||
print(f"⏸️ 2D视频已暂停: {video_screen.getName()}")
|
||||
# 在OpenCV中没有直接的暂停功能,我们通过停止线程来模拟暂停
|
||||
if video_screen.hasPythonTag("video_capture"):
|
||||
cap = video_screen.getPythonTag("video_capture")
|
||||
if cap:
|
||||
cap.release()
|
||||
video_screen.clearPythonTag("video_capture")
|
||||
print("⏸️ 视频已暂停")
|
||||
return True
|
||||
except Exception as stop_error:
|
||||
print(f"⚠️ 暂停2D视频时出错: {stop_error}")
|
||||
return False
|
||||
else:
|
||||
print(f"⚠️ 2D视频屏幕没有可暂停的视频纹理: {video_screen.getName()}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 暂停2D视频失败: {e}")
|
||||
print(f"❌ 暂停视频失败: {e}")
|
||||
return False
|
||||
|
||||
def stop2DVideo(self, video_screen):
|
||||
"""停止2D视频(回到开头)"""
|
||||
"""停止2D视频"""
|
||||
try:
|
||||
movie_texture = video_screen.getPythonTag("movie_texture")
|
||||
# 停止视频捕获
|
||||
if video_screen.hasPythonTag("video_capture"):
|
||||
cap = video_screen.getPythonTag("video_capture")
|
||||
if cap:
|
||||
cap.release()
|
||||
video_screen.clearPythonTag("video_capture")
|
||||
|
||||
# 备用获取方法
|
||||
if not movie_texture:
|
||||
# 尝试从frameTexture获取
|
||||
if hasattr(video_screen, 'frameTexture'):
|
||||
movie_texture = video_screen.frameTexture
|
||||
# 清理纹理
|
||||
if video_screen.hasPythonTag("video_texture"):
|
||||
video_screen.clearPythonTag("video_texture")
|
||||
|
||||
if movie_texture:
|
||||
try:
|
||||
if hasattr(movie_texture, 'stop'):
|
||||
movie_texture.stop()
|
||||
# 重置到开头
|
||||
if hasattr(movie_texture, 'setTime'):
|
||||
movie_texture.setTime(0.0)
|
||||
print(f"⏹️ 2D视频已停止: {video_screen.getName()}")
|
||||
# 清理视频路径标签
|
||||
video_screen.clearTag("video_path")
|
||||
|
||||
print("⏹️ 视频已停止")
|
||||
return True
|
||||
except Exception as stop_error:
|
||||
print(f"⚠️ 停止2D视频时出错: {stop_error}")
|
||||
except Exception as e:
|
||||
print(f"❌ 停止视频失败: {e}")
|
||||
return False
|
||||
|
||||
def createSphericalVideo(self, pos=(0, 0, 0), radius=5.0, video_path=None):
|
||||
"""创建球形视频(360度视频)- 支持无初始视频文件"""
|
||||
try:
|
||||
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
|
||||
from panda3d.core import Geom, GeomTriangles, GeomNode
|
||||
from panda3d.core import TextureStage, Texture
|
||||
import math
|
||||
import os
|
||||
|
||||
# 确保 pos 是有效的三维坐标元组
|
||||
if not isinstance(pos, (tuple, list)) or len(pos) != 3:
|
||||
print(f"⚠️ 位置参数无效,使用默认值 (0, 0, 0),原始值: {pos}")
|
||||
pos = (0, 0, 0)
|
||||
else:
|
||||
# 确保所有坐标都是数值类型
|
||||
try:
|
||||
pos = (float(pos[0]), float(pos[1]), float(pos[2]))
|
||||
except (ValueError, TypeError):
|
||||
print(f"⚠️ 位置参数包含非数值,使用默认值 (0, 0, 0),原始值: {pos}")
|
||||
pos = (0, 0, 0)
|
||||
|
||||
# 确保 radius 是有效数值
|
||||
try:
|
||||
radius = float(radius)
|
||||
except (ValueError, TypeError):
|
||||
print(f"⚠️ 半径参数无效,使用默认值 5.0,原始值: {radius}")
|
||||
radius = 5.0
|
||||
|
||||
print(f"🌍 开始创建球形视频,位置: {pos}, 半径: {radius}, 视频路径: {video_path}")
|
||||
|
||||
# 不再强制检查视频文件是否存在,允许创建空的球形视频
|
||||
if video_path and not os.path.exists(video_path):
|
||||
print(f"⚠️ 视频文件不存在,将创建空的球形视频: {video_path}")
|
||||
|
||||
# 获取树形控件
|
||||
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_spherical_videos = []
|
||||
|
||||
# 为每个有效的父节点创建球形视频
|
||||
for parent_item, parent_node in target_parents:
|
||||
try:
|
||||
# 生成唯一名称
|
||||
sphere_name = f"SphericalVideo_{len(self.gui_elements)}"
|
||||
|
||||
# 创建球形几何体
|
||||
sphere_geom = self._createSphereGeometry(radius, segments=32)
|
||||
|
||||
# 创建几何节点
|
||||
sphere_node = GeomNode('sphere_video')
|
||||
sphere_node.addGeom(sphere_geom)
|
||||
|
||||
# 创建节点路径并挂载
|
||||
sphere_np = parent_node.attachNewNode(sphere_node)
|
||||
sphere_np.setPos(*pos) # 现在 pos 已经是有效的元组
|
||||
sphere_np.setName(sphere_name)
|
||||
|
||||
# 翻转法线,使视频在球体内部显示
|
||||
sphere_np.setTwoSided(True)
|
||||
sphere_np.setBin('background', 0) # 确保在背景层
|
||||
sphere_np.setDepthWrite(False) # 不写入深度缓冲
|
||||
|
||||
# 设置初始颜色为占位符颜色
|
||||
sphere_np.setColor(0.1, 0.1, 0.3, 0.8) # 深蓝色占位符
|
||||
|
||||
# 如果提供了视频路径且文件存在,则加载视频纹理
|
||||
movie_texture = None
|
||||
if video_path and os.path.exists(video_path):
|
||||
try:
|
||||
# 加载视频纹理
|
||||
movie_texture = self._loadMovieTexture(video_path)
|
||||
if movie_texture:
|
||||
# 设置视频纹理属性
|
||||
movie_texture.setWrapU(Texture.WM_clamp)
|
||||
movie_texture.setWrapV(Texture.WM_clamp)
|
||||
movie_texture.setMinfilter(Texture.FT_linear)
|
||||
movie_texture.setMagfilter(Texture.FT_linear)
|
||||
|
||||
# 为视频纹理创建专用的纹理阶段
|
||||
texture_stage = TextureStage("spherical_video")
|
||||
texture_stage.setMode(TextureStage.MModulate)
|
||||
|
||||
# 应用纹理到球体
|
||||
sphere_np.setTexture(texture_stage, movie_texture)
|
||||
|
||||
# 设置为白色以正确显示视频
|
||||
sphere_np.setColor(1, 1, 1, 1)
|
||||
|
||||
print(f"✅ 视频纹理已应用到球形视频: {video_path}")
|
||||
|
||||
# 尝试自动播放
|
||||
try:
|
||||
if hasattr(movie_texture, 'play'):
|
||||
movie_texture.play()
|
||||
print("▶️ 球形视频已开始播放")
|
||||
except Exception as play_error:
|
||||
print(f"⚠️ 球形视频自动播放失败: {play_error}")
|
||||
else:
|
||||
print(f"⚠️ 无法加载视频纹理: {video_path}")
|
||||
# 保持占位符颜色
|
||||
except Exception as e:
|
||||
print(f"❌ 加载视频纹理失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# 保持占位符颜色
|
||||
else:
|
||||
if video_path:
|
||||
print(f"⚠️ 视频文件不存在: {video_path}")
|
||||
else:
|
||||
print("ℹ️ 未提供视频文件,创建空的球形视频")
|
||||
|
||||
# 设置标签以便识别和管理
|
||||
sphere_np.setTag("gui_type", "spherical_video")
|
||||
sphere_np.setTag("is_gui_element", "1")
|
||||
sphere_np.setTag("video_path", video_path or "")
|
||||
sphere_np.setTag("original_radius", str(radius))
|
||||
|
||||
# 保存视频纹理引用
|
||||
if movie_texture:
|
||||
sphere_np.setPythonTag("movie_texture", movie_texture)
|
||||
|
||||
# 添加到GUI元素列表
|
||||
self.gui_elements.append(sphere_np)
|
||||
|
||||
print(f"✅ 为 {parent_item.text(0)} 创建球形视频成功: {sphere_name}")
|
||||
|
||||
# 在Qt树形控件中添加对应节点
|
||||
qt_item = tree_widget.add_node_to_tree_widget(sphere_np, parent_item, "GUI_SPHERICAL_VIDEO")
|
||||
if qt_item:
|
||||
created_spherical_videos.append((sphere_np, qt_item))
|
||||
else:
|
||||
created_spherical_videos.append((sphere_np, None))
|
||||
print("⚠️ Qt树节点添加失败,但Panda3D对象已创建")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 为 {parent_item.text(0)} 创建球形视频失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
# 处理创建结果
|
||||
if not created_spherical_videos:
|
||||
print("❌ 没有成功创建任何球形视频")
|
||||
return None
|
||||
|
||||
# 选中最后创建的球形视频
|
||||
if created_spherical_videos:
|
||||
last_sphere_np, last_qt_item = created_spherical_videos[-1]
|
||||
if last_qt_item:
|
||||
tree_widget.setCurrentItem(last_qt_item)
|
||||
# 更新选择和属性面板
|
||||
tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item)
|
||||
|
||||
print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频")
|
||||
|
||||
# 返回值处理
|
||||
if len(created_spherical_videos) == 1:
|
||||
return created_spherical_videos[0][0] # 单个球形视频返回NodePath
|
||||
else:
|
||||
return [sphere_np for sphere_np, _ in created_spherical_videos] # 多个球形视频返回列表
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 创建球形视频过程失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _createSphereGeometry(self, radius=5.0, segments=32):
|
||||
"""创建球形几何体"""
|
||||
try:
|
||||
from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter
|
||||
from panda3d.core import Geom, GeomTriangles
|
||||
import math
|
||||
|
||||
# 创建顶点数据
|
||||
format = GeomVertexFormat.getV3n3t2()
|
||||
vdata = GeomVertexData('sphere', format, Geom.UHStatic)
|
||||
|
||||
vertex = GeomVertexWriter(vdata, 'vertex')
|
||||
normal = GeomVertexWriter(vdata, 'normal')
|
||||
texcoord = GeomVertexWriter(vdata, 'texcoord')
|
||||
|
||||
# 生成球体顶点
|
||||
vertices = []
|
||||
for i in range(segments + 1):
|
||||
phi = math.pi * i / segments # 从0到π
|
||||
for j in range(segments * 2 + 1):
|
||||
theta = 2 * math.pi * j / (segments * 2) # 从0到2π
|
||||
|
||||
x = radius * math.sin(phi) * math.cos(theta)
|
||||
y = radius * math.cos(phi)
|
||||
z = radius * math.sin(phi) * math.sin(theta)
|
||||
|
||||
# 纹理坐标
|
||||
u = j / (segments * 2)
|
||||
v = i / segments
|
||||
|
||||
# 修复:将坐标作为一个元组添加到列表中
|
||||
vertices.append((x, y, z, u, v))
|
||||
|
||||
# 添加顶点数据
|
||||
for vert in vertices:
|
||||
vertex.addData3f(vert[0], vert[1], vert[2])
|
||||
# 法线(标准化顶点位置)
|
||||
length = math.sqrt(vert[0] ** 2 + vert[1] ** 2 + vert[2] ** 2)
|
||||
if length > 0:
|
||||
normal.addData3f(vert[0] / length, vert[1] / length, vert[2] / length)
|
||||
else:
|
||||
normal.addData3f(0, 1, 0)
|
||||
texcoord.addData2f(vert[3], vert[4])
|
||||
|
||||
# 创建几何体
|
||||
geom = Geom(vdata)
|
||||
prim = GeomTriangles(Geom.UHStatic)
|
||||
|
||||
# 生成三角形面
|
||||
for i in range(segments):
|
||||
for j in range(segments * 2):
|
||||
# 计算顶点索引
|
||||
v1 = i * (segments * 2 + 1) + j
|
||||
v2 = (i + 1) * (segments * 2 + 1) + j
|
||||
v3 = (i + 1) * (segments * 2 + 1) + (j + 1)
|
||||
v4 = i * (segments * 2 + 1) + (j + 1)
|
||||
|
||||
# 添加两个三角形
|
||||
prim.addVertices(v1, v2, v3)
|
||||
prim.addVertices(v1, v3, v4)
|
||||
|
||||
prim.closePrimitive()
|
||||
geom.addPrimitive(prim)
|
||||
|
||||
return geom
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 创建球形几何体失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise
|
||||
|
||||
def playSphericalVideo(self,spherical_video_node):
|
||||
try:
|
||||
if not spherical_video_node:
|
||||
return False
|
||||
texture = spherical_video_node.getTexture()
|
||||
if texture and hasattr(texture,'play'):
|
||||
texture.play()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def pauseSphericalVideo(self,spherical_video_node):
|
||||
try:
|
||||
if not spherical_video_node:
|
||||
return False
|
||||
texture = spherical_video_node.getTexture()
|
||||
if texture and hasattr(texture,'stop'):
|
||||
texture.stop()
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def setSphericalVideoTime(self,spherical_video_node,time_seconds):
|
||||
try:
|
||||
if not spherical_video_node:
|
||||
return False
|
||||
texture = spherical_video_node.getTexture()
|
||||
if texture and hasattr(texture,'set_time'):
|
||||
texture.setTime(time_seconds)
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ 2D视频屏幕没有视频纹理: {video_screen.getName()}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 停止2D视频失败: {e}")
|
||||
return False
|
||||
|
||||
def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"):
|
||||
@ -2570,13 +2825,13 @@ class GUIManager:
|
||||
transform_layout.addWidget(z_label, 0, 2)
|
||||
|
||||
xPos = QDoubleSpinBox()
|
||||
xPos.setRange(-50, 50)
|
||||
xPos.setRange(-1000, 1000)
|
||||
xPos.setValue(logical_x)
|
||||
xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v))
|
||||
transform_layout.addWidget(xPos, 1, 1)
|
||||
|
||||
zPos = QDoubleSpinBox()
|
||||
zPos.setRange(-50, 50)
|
||||
zPos.setRange(-1000, 1000)
|
||||
zPos.setValue(logical_z)
|
||||
zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v))
|
||||
transform_layout.addWidget(zPos, 1, 2)
|
||||
@ -2930,19 +3185,13 @@ class GUIManager:
|
||||
if axis == "x":
|
||||
# 转换逻辑坐标到屏幕坐标
|
||||
screen_x = value * 0.1
|
||||
# 应用边界约束
|
||||
constrained_pos = self.constrain2DPosition(gui_element, new_x=screen_x)
|
||||
new_pos = (constrained_pos.getX(), constrained_pos.getY(), constrained_pos.getZ())
|
||||
new_pos = (screen_x,current_pos[1],current_pos[2])
|
||||
elif axis == "z":
|
||||
screen_z = value * 0.1
|
||||
# 应用边界约束
|
||||
constrained_pos = self.constrain2DPosition(gui_element, new_z=screen_z)
|
||||
new_pos = (constrained_pos.getX(), constrained_pos.getY(), constrained_pos.getZ())
|
||||
new_pos = (current_pos[0],current_pos[1],screen_z)
|
||||
else:
|
||||
return False
|
||||
|
||||
gui_element.setPos(*new_pos)
|
||||
print(f"✓ 更新2D GUI元素位置: {axis}={value} (约束后位置: {new_pos})")
|
||||
return True
|
||||
else:
|
||||
print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}")
|
||||
|
||||
13
main.py
13
main.py
@ -242,6 +242,19 @@ class MyWorld(CoreWorld):
|
||||
"""创建2D视频屏幕"""
|
||||
return self.gui_manager.createGUI2DVideoScreen(pos,size,video_path)
|
||||
|
||||
def createSphericalVideo(self,pos=(0,0,0),radius=5.0,video_path=None):
|
||||
"""创建360度视频"""
|
||||
return self.gui_manager.createSphericalVideo(pos,radius,video_path)
|
||||
|
||||
def playSphericalVideo(self,spherical_video_node):
|
||||
return self.gui_manager.playSphericalVideo(spherical_video_node)
|
||||
|
||||
def pauseSphericalVideo(self,spherical_video_node):
|
||||
return self.gui_manager.pauseSphericalVideo(spherical_video_node)
|
||||
|
||||
def setSphericalVideoTime(self,spherical_video_node,time_seconds):
|
||||
return self.gui_manager.setSphericalVideoTime(spherical_video_node,time_seconds)
|
||||
|
||||
def createSpotLight(self,pos=(0,0,5)):
|
||||
"""创建聚光灯"""
|
||||
return self.scene_manager.createSpotLight(pos)
|
||||
|
||||
@ -89,16 +89,8 @@ class SceneManager:
|
||||
# ==================== 模型导入和处理 ====================
|
||||
|
||||
def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True, auto_convert_to_glb=True):
|
||||
"""导入模型到场景 - 只在根节点下创建
|
||||
|
||||
Args:
|
||||
filepath: 模型文件路径
|
||||
apply_unit_conversion: 是否应用单位转换(主要针对FBX文件)
|
||||
normalize_scales: 是否标准化子节点缩放(推荐开启)
|
||||
auto_convert_to_glb: 是否自动将非GLB格式转换为GLB以获得更好的动画支持
|
||||
"""
|
||||
"""导入模型到场景 - 增强错误处理"""
|
||||
try:
|
||||
|
||||
# 预处理文件路径和转换
|
||||
filepath = util.normalize_model_path(filepath)
|
||||
original_filepath = filepath
|
||||
@ -107,11 +99,24 @@ class SceneManager:
|
||||
print(f"缩放标准化: {'开启' if normalize_scales else '关闭'}")
|
||||
print(f"自动转换GLB: {'开启' if auto_convert_to_glb else '关闭'}")
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(filepath):
|
||||
print(f"❌ 模型文件不存在: {filepath}")
|
||||
return None
|
||||
|
||||
# 检查文件大小
|
||||
file_size = os.path.getsize(filepath)
|
||||
if file_size == 0:
|
||||
print(f"❌ 模型文件为空: {filepath}")
|
||||
return None
|
||||
|
||||
print(f"文件大小: {file_size} 字节")
|
||||
|
||||
# 检查是否需要转换为GLB
|
||||
if auto_convert_to_glb and self._shouldConvertToGLB(filepath):
|
||||
print(f"🔄 检测到需要转换的格式,尝试转换为GLB...")
|
||||
converted_path = self._convertToGLBWithProgress(filepath)
|
||||
if converted_path:
|
||||
if converted_path and os.path.exists(converted_path) and os.path.getsize(converted_path) > 0:
|
||||
print(f"✅ 转换成功: {converted_path}")
|
||||
filepath = converted_path
|
||||
# 显示成功消息
|
||||
@ -123,16 +128,64 @@ class SceneManager:
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
print(f"⚠️ 转换失败,使用原始文件")
|
||||
print(f"⚠️ 转换失败或文件无效,使用原始文件")
|
||||
# 验证原始文件是否有效
|
||||
if not os.path.exists(original_filepath) or os.path.getsize(original_filepath) == 0:
|
||||
print(f"❌ 原始文件也无效: {original_filepath}")
|
||||
return None
|
||||
filepath = original_filepath
|
||||
|
||||
# 直接在render根节点下创建模型
|
||||
print("--- 在根节点下创建模型实例 ---")
|
||||
|
||||
# 加载模型
|
||||
print("直接从文件加载模型...")
|
||||
# 添加模型加载前的安全检查
|
||||
print("准备加载模型...")
|
||||
|
||||
# 使用try-except包装模型加载过程
|
||||
model = None
|
||||
max_retry_attempts = 3 # 最大重试次数
|
||||
|
||||
for attempt in range(max_retry_attempts):
|
||||
try:
|
||||
print(f"直接从文件加载模型... (尝试 {attempt + 1}/{max_retry_attempts})")
|
||||
model = self.world.loader.loadModel(filepath)
|
||||
|
||||
if model:
|
||||
print(f"✅ 模型加载成功: {model}")
|
||||
break
|
||||
else:
|
||||
print(f"⚠️ 加载模型返回空对象 (尝试 {attempt + 1})")
|
||||
if attempt < max_retry_attempts - 1:
|
||||
import time
|
||||
time.sleep(0.1) # 短暂延迟后重试
|
||||
|
||||
except AssertionError as ae:
|
||||
error_msg = str(ae)
|
||||
if "mismatched number of frames" in error_msg:
|
||||
print(f"⚠️ 动画帧数不匹配错误 (尝试 {attempt + 1}): {error_msg}")
|
||||
# 清理模型池并重试
|
||||
if attempt < max_retry_attempts - 1:
|
||||
self._clearModelCache(filepath)
|
||||
import time
|
||||
time.sleep(0.2) # 增加延迟后重试
|
||||
else:
|
||||
# 最后一次尝试失败,尝试无动画加载
|
||||
print("🔄 尝试无动画加载...")
|
||||
model = self._loadModelWithoutAnimations(filepath)
|
||||
else:
|
||||
raise ae # 其他断言错误直接抛出
|
||||
|
||||
except Exception as load_error:
|
||||
print(f"❌ 模型加载过程出错 (尝试 {attempt + 1}): {str(load_error)}")
|
||||
if attempt < max_retry_attempts - 1:
|
||||
import time
|
||||
time.sleep(0.1) # 短暂延迟后重试
|
||||
else:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if not model:
|
||||
print("❌ 加载模型失败")
|
||||
print("❌ 所有加载尝试都失败了")
|
||||
return None
|
||||
|
||||
# 设置模型名称
|
||||
@ -154,17 +207,6 @@ class SceneManager:
|
||||
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")
|
||||
|
||||
# 应用处理选项
|
||||
# 对于GLB文件,通常不需要单位转换,因为它们已经是标准单位
|
||||
if apply_unit_conversion and filepath.lower().endswith(
|
||||
@ -228,6 +270,49 @@ class SceneManager:
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def _clearModelCache(self, filepath):
|
||||
"""清理模型缓存以解决加载问题"""
|
||||
try:
|
||||
# 清理特定文件的缓存
|
||||
filename = Filename.fromOsSpecific(filepath)
|
||||
if ModelPool.hasModel(filename):
|
||||
ModelPool.releaseModel(filename)
|
||||
print(f"🧹 已清理模型缓存: {filepath}")
|
||||
|
||||
# 清理所有动画缓存
|
||||
from panda3d.core import AnimBundleNode
|
||||
AnimBundleNode.clearAllBundleCache()
|
||||
print("🧹 已清理动画缓存")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 清理缓存时出错: {e}")
|
||||
|
||||
def _loadModelWithoutAnimations(self, filepath):
|
||||
"""尝试无动画加载模型"""
|
||||
try:
|
||||
from panda3d.core import LoaderOptions
|
||||
|
||||
print("🔄 尝试无动画加载模型...")
|
||||
|
||||
# 创建不加载动画的加载选项
|
||||
loader_options = LoaderOptions()
|
||||
loader_options.setFlags(LoaderOptions.LF_no_cache) # 不使用缓存
|
||||
loader_options.setAllowInstance(False) # 不允许实例化
|
||||
|
||||
# 尝试加载不带动画的模型
|
||||
model = self.world.loader.loadModel(filepath, loaderOptions=loader_options)
|
||||
|
||||
if model:
|
||||
print("✅ 无动画加载成功")
|
||||
return model
|
||||
else:
|
||||
print("❌ 无动画加载也失败了")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 无动画加载失败: {e}")
|
||||
return None
|
||||
|
||||
# def importAnimatedFBX(self, filepath, scale=0.01, auto_play=True):
|
||||
# """导入带动画的FBX模型(使用assimp)
|
||||
#
|
||||
@ -289,8 +374,10 @@ class SceneManager:
|
||||
|
||||
def _applyMaterialsToModel(self, model):
|
||||
"""递归应用材质到模型的所有GeomNode"""
|
||||
|
||||
def apply_material(node_path, depth=0):
|
||||
indent = " " * depth
|
||||
try:
|
||||
print(f"{indent}处理节点: {node_path.getName()}")
|
||||
print(f"{indent}节点类型: {node_path.node().__class__.__name__}")
|
||||
|
||||
@ -322,19 +409,24 @@ class SceneManager:
|
||||
# 如果还没找到颜色,检查几何体
|
||||
if not has_color:
|
||||
for i in range(geom_node.getNumGeoms()):
|
||||
try:
|
||||
geom = geom_node.getGeom(i)
|
||||
state = geom_node.getGeomState(i)
|
||||
|
||||
# 检查顶点颜色
|
||||
vdata = geom.getVertexData()
|
||||
if vdata:
|
||||
format = vdata.getFormat()
|
||||
if format:
|
||||
for j in range(format.getNumColumns()):
|
||||
try:
|
||||
column = format.getColumn(j)
|
||||
# InternalName对象需要使用getName()转换为字符串
|
||||
column_name = column.getName().getName()
|
||||
if "color" in column_name.lower():
|
||||
print(f"{indent}发现顶点颜色数据: {column_name}")
|
||||
# 这里可以读取顶点颜色,但先记录发现
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 检查材质属性
|
||||
if state.hasAttrib(MaterialAttrib.getClassType()):
|
||||
@ -360,14 +452,21 @@ class SceneManager:
|
||||
has_color = True
|
||||
print(f"{indent}从颜色属性获取: {color}")
|
||||
break
|
||||
except Exception as geom_error:
|
||||
print(f"{indent}处理几何体 {i} 时出错: {geom_error}")
|
||||
continue
|
||||
|
||||
# 创建新材质
|
||||
material = Material()
|
||||
if has_color:
|
||||
print(f"{indent}应用找到的颜色: {color}")
|
||||
try:
|
||||
material.setDiffuse(color)
|
||||
material.setBaseColor(color) # 同时设置基础颜色
|
||||
node_path.setColor(color)
|
||||
except Exception as color_error:
|
||||
print(f"{indent}设置颜色时出错: {color_error}")
|
||||
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
|
||||
else:
|
||||
print(f"{indent}使用默认颜色")
|
||||
material.setDiffuse((0.8, 0.8, 0.8, 1.0))
|
||||
@ -376,23 +475,31 @@ class SceneManager:
|
||||
material.setAmbient((0.2, 0.2, 0.2, 1.0))
|
||||
material.setSpecular((0.5, 0.5, 0.5, 1.0))
|
||||
material.setShininess(32.0)
|
||||
#material.set_metallic(1)
|
||||
#material.set_roughness(0)
|
||||
|
||||
# 应用材质
|
||||
node_path.setMaterial(material)
|
||||
print(f"{indent}几何体数量: {geom_node.getNumGeoms()}")
|
||||
|
||||
except Exception as node_error:
|
||||
print(f"{indent}处理节点 {node_path.getName()} 时出错: {node_error}")
|
||||
|
||||
# 递归处理子节点
|
||||
child_count = node_path.getNumChildren()
|
||||
print(f"{indent}子节点数量: {child_count}")
|
||||
for i in range(child_count):
|
||||
try:
|
||||
child = node_path.getChild(i)
|
||||
apply_material(child, depth + 1)
|
||||
except Exception as child_error:
|
||||
print(f"{indent}处理子节点 {i} 时出错: {child_error}")
|
||||
continue
|
||||
|
||||
# 应用材质
|
||||
print("\n开始递归应用材质...")
|
||||
try:
|
||||
apply_material(model)
|
||||
except Exception as e:
|
||||
print(f"应用材质时出错: {e}")
|
||||
print("=== 材质设置完成 ===\n")
|
||||
|
||||
def _adjustModelToGround(self, model):
|
||||
|
||||
@ -429,7 +429,7 @@ class MainWindow(QMainWindow):
|
||||
self.createImageAction.triggered.connect(lambda: self.world.createGUI2DImage())
|
||||
self.createVideoScreen.triggered.connect(self.world.createVideoScreen)
|
||||
self.create2DVideoScreen.triggered.connect(self.world.create2DVideoScreen)
|
||||
# self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo)
|
||||
self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo)
|
||||
self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen())
|
||||
self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight())
|
||||
self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight())
|
||||
@ -573,10 +573,10 @@ class MainWindow(QMainWindow):
|
||||
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)
|
||||
# self.consoleDock = QDockWidget("控制台", self)
|
||||
# self.consoleView = CustomConsoleDockWidget(self.world)
|
||||
# self.consoleDock.setWidget(self.consoleView)
|
||||
# self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock)
|
||||
|
||||
def setupToolbar(self):
|
||||
"""创建工具栏"""
|
||||
|
||||
@ -24,6 +24,7 @@ class PropertyPanelManager:
|
||||
self.world = world
|
||||
self._propertyLayout = None
|
||||
self._actor_cache={}
|
||||
self._spherical_video_controls = {}
|
||||
|
||||
# 初始化地形编辑参数
|
||||
if not hasattr(self.world, 'terrain_edit_radius'):
|
||||
@ -1325,13 +1326,13 @@ class PropertyPanelManager:
|
||||
transform_layout.addWidget(z_label, 0, 2)
|
||||
|
||||
self.pos_x = QDoubleSpinBox()
|
||||
self.pos_x.setRange(-100, 100)
|
||||
self.pos_x.setRange(-1000, 1000)
|
||||
self.pos_x.setValue(logical_x)
|
||||
self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v))
|
||||
transform_layout.addWidget(self.pos_x, 1, 1)
|
||||
|
||||
self.pos_z = QDoubleSpinBox()
|
||||
self.pos_z.setRange(-100, 100)
|
||||
self.pos_z.setRange(-1000, 1000)
|
||||
self.pos_z.setValue(logical_z)
|
||||
self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v))
|
||||
transform_layout.addWidget(self.pos_z, 1, 2)
|
||||
@ -1610,6 +1611,8 @@ class PropertyPanelManager:
|
||||
self._addVideoScreenProperties(gui_element)
|
||||
if gui_type == "2d_video_screen":
|
||||
self._add2DVideoScreenProperties(gui_element)
|
||||
if gui_type == "spherical_video":
|
||||
self._addSphericalVideoProperties(gui_element)
|
||||
|
||||
self._propertyLayout.addStretch()
|
||||
|
||||
@ -1620,11 +1623,314 @@ class PropertyPanelManager:
|
||||
if propertyWidget:
|
||||
propertyWidget.update()
|
||||
|
||||
def _addSphericalVideoProperties(self, spherical_video):
|
||||
"""为球形视频添加属性控制面板"""
|
||||
try:
|
||||
from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel,
|
||||
QDoubleSpinBox, QFileDialog, QHBoxLayout, QSlider)
|
||||
from PyQt5.QtCore import Qt
|
||||
import os
|
||||
|
||||
# 视频控制组
|
||||
video_group = QGroupBox("视频控制")
|
||||
video_layout = QGridLayout()
|
||||
|
||||
# 播放控制按钮
|
||||
control_layout = QHBoxLayout()
|
||||
|
||||
self.play_button = QPushButton("▶️ 播放")
|
||||
self.play_button.clicked.connect(lambda: self._toggleSphericalVideoPlayback(spherical_video))
|
||||
|
||||
self.stop_button = QPushButton("⏹️ 停止")
|
||||
self.stop_button.clicked.connect(lambda: self._stopSphericalVideo(spherical_video))
|
||||
|
||||
control_layout.addWidget(self.play_button)
|
||||
control_layout.addWidget(self.stop_button)
|
||||
video_layout.addLayout(control_layout, 0, 0, 1, 2)
|
||||
|
||||
# 视频进度控制
|
||||
progress_layout = QHBoxLayout()
|
||||
self.progress_slider = QSlider(Qt.Horizontal)
|
||||
self.progress_slider.setRange(0, 100)
|
||||
self.progress_slider.setValue(0)
|
||||
self.progress_slider.sliderMoved.connect(lambda value: self._seekSphericalVideo(spherical_video, value))
|
||||
|
||||
self.time_label = QLabel("00:00 / 00:00")
|
||||
self.time_label.setStyleSheet("font-size: 10px; color: gray;")
|
||||
|
||||
progress_layout.addWidget(self.progress_slider)
|
||||
progress_layout.addWidget(self.time_label)
|
||||
video_layout.addLayout(progress_layout, 1, 0, 1, 2)
|
||||
|
||||
# 视频文件选择
|
||||
file_layout = QHBoxLayout()
|
||||
self.select_video_button = QPushButton("选择视频文件")
|
||||
self.select_video_button.clicked.connect(lambda: self._selectSphericalVideoFile(spherical_video))
|
||||
|
||||
# 显示当前视频文件
|
||||
current_video = spherical_video.getTag("video_path") if spherical_video.hasTag("video_path") else ""
|
||||
self.video_file_label = QLabel(os.path.basename(current_video) if current_video else "未选择视频")
|
||||
self.video_file_label.setStyleSheet("font-size: 9px; color: gray;")
|
||||
self.video_file_label.setWordWrap(True)
|
||||
|
||||
file_layout.addWidget(self.select_video_button)
|
||||
file_layout.addWidget(self.video_file_label)
|
||||
video_layout.addLayout(file_layout, 2, 0, 1, 2)
|
||||
|
||||
video_group.setLayout(video_layout)
|
||||
self._propertyLayout.addWidget(video_group)
|
||||
|
||||
# 球体属性组
|
||||
sphere_group = QGroupBox("球体属性")
|
||||
sphere_layout = QGridLayout()
|
||||
|
||||
# 半径控制
|
||||
sphere_layout.addWidget(QLabel("半径:"), 0, 0)
|
||||
radius_spin = QDoubleSpinBox()
|
||||
radius_spin.setRange(0.1, 100)
|
||||
radius_spin.setSingleStep(0.1)
|
||||
current_radius = float(spherical_video.getTag("original_radius") or "5.0")
|
||||
radius_spin.setValue(current_radius)
|
||||
radius_spin.valueChanged.connect(lambda value: self._updateSphericalVideoRadius(spherical_video, value))
|
||||
sphere_layout.addWidget(radius_spin, 0, 1)
|
||||
|
||||
sphere_group.setLayout(sphere_layout)
|
||||
self._propertyLayout.addWidget(sphere_group)
|
||||
|
||||
# 存储控件引用
|
||||
self._spherical_video_controls = {
|
||||
'play_button': self.play_button,
|
||||
'stop_button': self.stop_button,
|
||||
'progress_slider': self.progress_slider,
|
||||
'time_label': self.time_label,
|
||||
'select_button': self.select_video_button,
|
||||
'file_label': self.video_file_label
|
||||
}
|
||||
|
||||
print("✅ 球形视频属性面板已添加")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 添加球形视频属性面板失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _toggleSphericalVideoPlayback(self, spherical_video):
|
||||
"""切换球形视频播放状态"""
|
||||
try:
|
||||
# 获取视频纹理
|
||||
movie_texture = spherical_video.getPythonTag("movie_texture")
|
||||
if not movie_texture:
|
||||
# 尝试从节点获取纹理
|
||||
texture = spherical_video.getTexture()
|
||||
if texture:
|
||||
movie_texture = texture
|
||||
|
||||
if not movie_texture:
|
||||
print("❌ 未找到视频纹理")
|
||||
return
|
||||
|
||||
# 检查当前播放状态
|
||||
if not hasattr(self, '_video_playing'):
|
||||
self._video_playing = {}
|
||||
|
||||
video_id = id(spherical_video)
|
||||
is_playing = self._video_playing.get(video_id, False)
|
||||
|
||||
if is_playing:
|
||||
# 暂停视频
|
||||
if hasattr(movie_texture, 'stop'):
|
||||
movie_texture.stop()
|
||||
if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls:
|
||||
play_button = self._spherical_video_controls['play_button']
|
||||
play_button.setText("▶️ 播放")
|
||||
print("⏸️ 球形视频已暂停")
|
||||
else:
|
||||
# 播放视频
|
||||
if hasattr(movie_texture, 'play'):
|
||||
movie_texture.play()
|
||||
if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls:
|
||||
play_button = self._spherical_video_controls['play_button']
|
||||
play_button.setText("⏸️ 暂停")
|
||||
print("▶️ 球形视频开始播放")
|
||||
|
||||
# 更新播放状态
|
||||
self._video_playing[video_id] = not is_playing
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 切换球形视频播放状态失败: {e}")
|
||||
|
||||
def _stopSphericalVideo(self, spherical_video):
|
||||
"""停止球形视频"""
|
||||
try:
|
||||
# 获取视频纹理
|
||||
movie_texture = spherical_video.getPythonTag("movie_texture")
|
||||
if not movie_texture:
|
||||
# 尝试从节点获取纹理
|
||||
texture = spherical_video.getTexture()
|
||||
if texture:
|
||||
movie_texture = texture
|
||||
|
||||
if movie_texture:
|
||||
if hasattr(movie_texture, 'stop'):
|
||||
movie_texture.stop()
|
||||
# 重置到开头
|
||||
if hasattr(movie_texture, 'setTime'):
|
||||
movie_texture.setTime(0.0)
|
||||
|
||||
# 更新播放按钮
|
||||
if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls:
|
||||
play_button = self._spherical_video_controls['play_button']
|
||||
play_button.setText("▶️ 播放")
|
||||
|
||||
# 更新播放状态
|
||||
if hasattr(self, '_video_playing'):
|
||||
video_id = id(spherical_video)
|
||||
self._video_playing[video_id] = False
|
||||
|
||||
print("⏹️ 球形视频已停止")
|
||||
else:
|
||||
print("❌ 视频纹理不支持停止操作")
|
||||
else:
|
||||
print("❌ 未找到视频纹理")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 停止球形视频失败: {e}")
|
||||
|
||||
def _seekSphericalVideo(self, spherical_video, slider_value):
|
||||
"""跳转到指定时间位置"""
|
||||
try:
|
||||
# 获取视频纹理
|
||||
movie_texture = spherical_video.getPythonTag("movie_texture")
|
||||
if not movie_texture:
|
||||
# 尝试从节点获取纹理
|
||||
texture = spherical_video.getTexture()
|
||||
if texture:
|
||||
movie_texture = texture
|
||||
|
||||
if movie_texture and hasattr(movie_texture, 'setTime'):
|
||||
# 假设视频总时长为100秒(实际应该根据视频时长计算)
|
||||
time_seconds = slider_value
|
||||
movie_texture.setTime(time_seconds)
|
||||
print(f"⏰ 跳转到时间: {time_seconds}秒")
|
||||
|
||||
# 更新时间标签
|
||||
if hasattr(self, '_spherical_video_controls') and 'time_label' in self._spherical_video_controls:
|
||||
time_label = self._spherical_video_controls['time_label']
|
||||
current_time = f"{int(time_seconds // 60):02d}:{int(time_seconds % 60):02d}"
|
||||
total_time = "01:40" # 假设总时长100秒
|
||||
time_label.setText(f"{current_time} / {total_time}")
|
||||
|
||||
else:
|
||||
print("❌ 视频纹理不支持时间控制")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 球形视频跳转失败: {e}")
|
||||
|
||||
def _updateSphericalVideoRadius(self, spherical_video, new_radius):
|
||||
"""更新球形视频半径"""
|
||||
try:
|
||||
from panda3d.core import GeomNode
|
||||
|
||||
# 移除旧的几何体
|
||||
spherical_video.node().removeAllGeoms()
|
||||
|
||||
# 创建新的球形几何体
|
||||
new_geom = self.world.gui_manager._createSphereGeometry(new_radius, segments=32)
|
||||
spherical_video.node().addGeom(new_geom)
|
||||
|
||||
# 更新标签
|
||||
spherical_video.setTag("original_radius", str(new_radius))
|
||||
|
||||
print(f"🔄 球形视频半径已更新为: {new_radius}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 更新球形视频半径失败: {e}")
|
||||
|
||||
def loadSphericalVideoFile(self, spherical_video, video_path):
|
||||
"""为球形视频加载新的视频文件"""
|
||||
try:
|
||||
from panda3d.core import TextureStage, Texture
|
||||
import os
|
||||
|
||||
if not os.path.exists(video_path):
|
||||
print(f"❌ 球形视频文件不存在: {video_path}")
|
||||
return False
|
||||
|
||||
# 加载新的视频纹理
|
||||
movie_texture = self.world.gui_manager._loadMovieTexture(video_path)
|
||||
if movie_texture:
|
||||
# 清除现有的纹理
|
||||
spherical_video.clearTexture()
|
||||
|
||||
# 设置视频纹理属性
|
||||
movie_texture.setWrapU(Texture.WM_clamp)
|
||||
movie_texture.setWrapV(Texture.WM_clamp)
|
||||
movie_texture.setMinfilter(Texture.FT_linear)
|
||||
movie_texture.setMagfilter(Texture.FT_linear)
|
||||
|
||||
# 为视频纹理创建专用的纹理阶段
|
||||
texture_stage = TextureStage("spherical_video")
|
||||
texture_stage.setMode(TextureStage.MModulate)
|
||||
|
||||
# 应用纹理到球体
|
||||
spherical_video.setTexture(texture_stage, movie_texture)
|
||||
|
||||
# 保存视频纹理引用
|
||||
spherical_video.setPythonTag("movie_texture", movie_texture)
|
||||
spherical_video.setTag("video_path", video_path)
|
||||
|
||||
# 设置为白色以正确显示视频
|
||||
spherical_video.setColor(1, 1, 1, 1)
|
||||
|
||||
print(f"✅ 成功加载新球形视频: {video_path}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 无法加载球形视频文件: {video_path}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 加载球形视频文件失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _selectSphericalVideoFile(self, spherical_video):
|
||||
"""选择球形视频文件"""
|
||||
try:
|
||||
from PyQt5.QtWidgets import QFileDialog
|
||||
import os
|
||||
|
||||
# 打开文件选择对话框
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
None,
|
||||
"选择360度视频文件",
|
||||
"",
|
||||
"视频文件 (*.mp4 *.avi *.mov *.wmv *.flv *.mkv *.webm);;所有文件 (*)"
|
||||
)
|
||||
|
||||
if file_path and os.path.exists(file_path):
|
||||
# 加载新视频文件 - 修复方法调用
|
||||
success = self.loadSphericalVideoFile(spherical_video, file_path)
|
||||
if success:
|
||||
# 更新文件标签显示
|
||||
if hasattr(self, '_spherical_video_controls') and 'file_label' in self._spherical_video_controls:
|
||||
file_label = self._spherical_video_controls['file_label']
|
||||
file_label.setText(os.path.basename(file_path))
|
||||
print(f"✅ 球形视频文件已更新为: {file_path}")
|
||||
else:
|
||||
print(f"❌ 无法加载球形视频文件: {file_path}")
|
||||
else:
|
||||
print("❌ 未选择有效的视频文件")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 选择球形视频文件失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
def _add2DVideoScreenProperties(self, video_screen):
|
||||
"""为2D视频屏幕添加属性控制面板"""
|
||||
try:
|
||||
from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel,
|
||||
QFileDialog, QHBoxLayout)
|
||||
QFileDialog, QHBoxLayout, QLineEdit)
|
||||
import os
|
||||
|
||||
# 视频信息组
|
||||
@ -1632,18 +1938,39 @@ class PropertyPanelManager:
|
||||
video_info_layout = QGridLayout()
|
||||
|
||||
# 显示当前视频文件路径
|
||||
# 在显示视频信息时区分本地文件和网络URL
|
||||
video_path = video_screen.getTag("video_path")
|
||||
if video_path and os.path.exists(video_path):
|
||||
if video_path:
|
||||
if video_path.startswith("http://") or video_path.startswith("https://"):
|
||||
# 显示URL信息
|
||||
video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0)
|
||||
display_url = video_path if len(video_path) <= 50 else video_path[:47]+"..."
|
||||
path_label = QLabel(display_url)
|
||||
path_label.setWordWrap(True)
|
||||
path_label.setStyleSheet("color: #00AAFF;")
|
||||
video_info_layout.addWidget(path_label, 0, 1)
|
||||
elif os.path.exists(video_path):
|
||||
# 显示本地文件信息
|
||||
video_info_layout.addWidget(QLabel("视频文件:"), 0, 0)
|
||||
path_label = QLabel(os.path.basename(video_path))
|
||||
filename = os.path.basename(video_path)
|
||||
display_filename = filename if len(filename) <= 30 else filename[:27]+"..."
|
||||
path_label = QLabel(display_filename)
|
||||
path_label.setWordWrap(True)
|
||||
path_label.setStyleSheet("color: #00AAFF;")
|
||||
video_info_layout.addWidget(path_label, 0, 1)
|
||||
else:
|
||||
# 文件不存在
|
||||
video_info_layout.addWidget(QLabel("状态:"), 0, 0)
|
||||
status_label = QLabel("无视频文件" if not video_path else "文件不存在")
|
||||
status_label = QLabel("文件不存在")
|
||||
status_label.setStyleSheet("color: red;")
|
||||
video_info_layout.addWidget(status_label, 0, 1)
|
||||
else:
|
||||
# 无视频
|
||||
video_info_layout.addWidget(QLabel("状态:"), 0, 0)
|
||||
status_label = QLabel("无视频文件")
|
||||
status_label.setStyleSheet("color: red;")
|
||||
video_info_layout.addWidget(status_label, 0, 1)
|
||||
|
||||
video_screen.setBin("fixed", 0)
|
||||
|
||||
video_info_group.setLayout(video_info_layout)
|
||||
@ -1655,17 +1982,17 @@ class PropertyPanelManager:
|
||||
|
||||
# 播放按钮
|
||||
play_btn = QPushButton("▶️ 播放")
|
||||
play_btn.clicked.connect(lambda: self.world.gui_manager.play2DVideo(video_screen))
|
||||
play_btn.clicked.connect(lambda: self.play2DVideo(video_screen))
|
||||
video_control_layout.addWidget(play_btn)
|
||||
|
||||
# 暂停按钮
|
||||
pause_btn = QPushButton("⏸️ 暂停")
|
||||
pause_btn.clicked.connect(lambda: self.world.gui_manager.pause2DVideo(video_screen))
|
||||
pause_btn.clicked.connect(lambda: self.pause2DVideo(video_screen))
|
||||
video_control_layout.addWidget(pause_btn)
|
||||
|
||||
# 停止按钮
|
||||
stop_btn = QPushButton("⏹️ 停止")
|
||||
stop_btn.clicked.connect(lambda: self.world.gui_manager.stop2DVideo(video_screen))
|
||||
stop_btn.clicked.connect(lambda: self._stopVideo(video_screen))
|
||||
video_control_layout.addWidget(stop_btn)
|
||||
|
||||
video_control_group.setLayout(video_control_layout)
|
||||
@ -1676,9 +2003,295 @@ class PropertyPanelManager:
|
||||
load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen))
|
||||
self._propertyLayout.addWidget(load_btn)
|
||||
|
||||
# 添加URL输入区域
|
||||
url_group = QGroupBox("网络视频")
|
||||
url_layout = QHBoxLayout()
|
||||
|
||||
self.url_input = QLineEdit()
|
||||
self.url_input.setPlaceholderText("输入视频流URL")
|
||||
if video_path and (video_path.startswith("http://") or video_path.startswith("https://")):
|
||||
self.url_input.setText(video_path)
|
||||
url_layout.addWidget(self.url_input)
|
||||
|
||||
load_url_btn = QPushButton("加载URL")
|
||||
# 修复:直接调用 _loadVideoFromURL 方法并传递 video_screen 和 URL 文本
|
||||
load_url_btn.clicked.connect(lambda: self._loadVideoFromURL(video_screen, self.url_input.text().strip()))
|
||||
url_layout.addWidget(load_url_btn)
|
||||
|
||||
url_group.setLayout(url_layout)
|
||||
self._propertyLayout.addWidget(url_group)
|
||||
|
||||
except Exception as e:
|
||||
print(f"添加2D视频屏幕属性失败: {e}")
|
||||
|
||||
def _loadVideoFromURL(self, video_screen, url):
|
||||
"""从URL加载视频流并在2D视频屏幕上显示(使用MovieTexture)"""
|
||||
if not url:
|
||||
print("❌ URL不能为空")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 停止之前可能正在播放的视频
|
||||
self._stopVideo(video_screen)
|
||||
|
||||
# 检查是本地文件还是网络URL
|
||||
if url.startswith("http://") or url.startswith("https://"):
|
||||
# 对于网络流,我们仍然使用OpenCV方式
|
||||
return self._loadVideoFromURLWithOpenCV(video_screen, url)
|
||||
else:
|
||||
# 对于本地文件,使用与_loadNew2DVideo相同的方式
|
||||
return self.world.gui_manager.load2DVideoFile(video_screen, url)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 加载视频失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _loadVideoFromURLWithOpenCV(self, video_screen, url):
|
||||
"""使用OpenCV从URL加载视频流并在2D视频屏幕上显示(稳定版)"""
|
||||
try:
|
||||
import cv2
|
||||
import threading
|
||||
from panda3d.core import Texture, PNMImage
|
||||
import time
|
||||
|
||||
# 停止之前可能正在播放的视频
|
||||
self._stopVideo(video_screen)
|
||||
|
||||
# 使用 OpenCV 打开视频流
|
||||
cap = cv2.VideoCapture(url)
|
||||
if not cap.isOpened():
|
||||
print(f"❌ 无法打开视频流: {url}")
|
||||
return False
|
||||
|
||||
# 优化视频参数设置
|
||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟
|
||||
cap.set(cv2.CAP_PROP_FPS, 20) # 设置合理的帧率
|
||||
|
||||
# 创建纹理对象
|
||||
texture = Texture("video_texture")
|
||||
texture.setMagfilter(Texture.FTLinear)
|
||||
texture.setMinfilter(Texture.FTLinear)
|
||||
texture.setWrapU(Texture.WMClamp)
|
||||
texture.setWrapV(Texture.WMClamp)
|
||||
|
||||
# 保存视频信息
|
||||
video_info = {
|
||||
'capture': cap,
|
||||
'texture': texture,
|
||||
'playing': True,
|
||||
'target_fps': 60,
|
||||
'last_frame_time': time.time()
|
||||
}
|
||||
|
||||
# 应用纹理到视频屏幕
|
||||
video_screen.setTexture(texture, 1)
|
||||
video_screen.setPythonTag("video_info", video_info)
|
||||
video_screen.setTag("video_path", url)
|
||||
|
||||
# 启动视频播放线程
|
||||
def update_video_texture():
|
||||
frame_skip_counter = 0
|
||||
#frame_skip_rate = 1 # 每处理1帧就跳过2帧
|
||||
|
||||
while True:
|
||||
try:
|
||||
current_time = time.time()
|
||||
|
||||
# 检查是否应该继续播放
|
||||
if not (video_screen.hasPythonTag("video_info") and
|
||||
video_screen.getPythonTag("video_info").get('playing', False)):
|
||||
break
|
||||
|
||||
video_info = video_screen.getPythonTag("video_info")
|
||||
cap = video_info['capture']
|
||||
target_fps = video_info.get('target_fps', 20)
|
||||
frame_time = 1.0 / target_fps
|
||||
|
||||
# 控制帧率,避免过度处理
|
||||
if current_time - video_info['last_frame_time'] < frame_time:
|
||||
time.sleep(0.005) # 短暂休眠
|
||||
continue
|
||||
|
||||
# 读取帧
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
# 视频结束,重新开始播放
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
continue
|
||||
|
||||
# 帧跳过机制
|
||||
# frame_skip_counter = (frame_skip_counter + 1) % frame_skip_rate
|
||||
# if frame_skip_counter != 0:
|
||||
# time.sleep(0.01) # 短暂休眠
|
||||
# continue
|
||||
|
||||
# 更新最后处理时间
|
||||
video_info['last_frame_time'] = current_time
|
||||
|
||||
# 调整帧大小以降低处理负担
|
||||
frame_height, frame_width = frame.shape[:2]
|
||||
if frame_width > 256: # 限制最大宽度
|
||||
scale = 256.0 / frame_width
|
||||
new_width = int(frame_width * scale)
|
||||
new_height = int(frame_height * scale)
|
||||
frame = cv2.resize(frame, (new_width, new_height))
|
||||
|
||||
# 转换颜色格式 (BGR to RGB)
|
||||
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
|
||||
# 获取帧尺寸
|
||||
height, width = frame_rgb.shape[:2]
|
||||
|
||||
# 创建 PNMImage 并设置数据
|
||||
img = PNMImage(width, height, 3)
|
||||
img.set_maxval(255)
|
||||
|
||||
# 使用逐行设置方法(稳定且兼容性好)
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
r, g, b = frame_rgb[y, x]
|
||||
img.setXelVal(x, y, r, g, b)
|
||||
|
||||
# 更新纹理
|
||||
texture = video_info['texture']
|
||||
if texture:
|
||||
texture.load(img)
|
||||
|
||||
except Exception as e:
|
||||
print(f"视频帧更新错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# 出现错误时短暂休眠,防止CPU占用过高
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# 线程结束时释放资源
|
||||
try:
|
||||
if 'capture' in video_info and video_info['capture']:
|
||||
video_info['capture'].release()
|
||||
except:
|
||||
pass
|
||||
|
||||
print("视频播放线程结束")
|
||||
|
||||
# 在单独线程中处理视频流
|
||||
thread = threading.Thread(target=update_video_texture, daemon=True)
|
||||
thread.start()
|
||||
|
||||
print(f"✅ 开始在2D视频屏幕上播放网络视频流: {url}")
|
||||
return True
|
||||
|
||||
except ImportError:
|
||||
print("❌ 缺少必要的库,请安装: pip install opencv-python")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 加载网络视频失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def play2DVideo(self, video_screen):
|
||||
"""播放2D视频"""
|
||||
try:
|
||||
# 检查是否已经有视频信息(OpenCV方式)
|
||||
if video_screen.hasPythonTag("video_info"):
|
||||
video_info = video_screen.getPythonTag("video_info")
|
||||
if video_info:
|
||||
video_info['playing'] = True
|
||||
print("▶️ 视频已恢复播放")
|
||||
return True
|
||||
|
||||
# 检查是否是 MovieTexture 方式
|
||||
movie_texture = video_screen.getPythonTag("movie_texture")
|
||||
if movie_texture and hasattr(movie_texture, 'play'):
|
||||
try:
|
||||
movie_texture.play()
|
||||
print("▶️ MovieTexture 视频开始播放")
|
||||
return True
|
||||
except Exception as play_error:
|
||||
print(f"⚠️ MovieTexture 播放视频时出错: {play_error}")
|
||||
|
||||
url = None
|
||||
if hasattr(self,'url_input') and self.url_input:
|
||||
url = self.url_input.text().strip()
|
||||
if not url or not url.startswith(("http://","https://")):
|
||||
url = video_screen.getTag("video_path")
|
||||
|
||||
if url:
|
||||
if url.startswith("http://") or url.startswith("htpps://"):
|
||||
return self._loadVideoFromURLWithOpenCV(video_screen,url)
|
||||
else:
|
||||
return self.world.gui_manager.load2DVideoFile(video_screen,url)
|
||||
else:
|
||||
print("没有找到视频源")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 播放视频失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def pause2DVideo(self, video_screen):
|
||||
"""暂停2D视频"""
|
||||
try:
|
||||
if video_screen.hasPythonTag("video_info"):
|
||||
video_info = video_screen.getPythonTag("video_info")
|
||||
if video_info:
|
||||
video_info['playing'] = False
|
||||
print("⏸️ 视频已暂停")
|
||||
return True
|
||||
print("没有正在播放的视频")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 暂停视频失败: {e}")
|
||||
return False
|
||||
|
||||
def _stopVideo(self, video_screen):
|
||||
"""停止2D视频(内部方法)"""
|
||||
try:
|
||||
# 停止视频播放
|
||||
if video_screen.hasPythonTag("video_info"):
|
||||
video_info = video_screen.getPythonTag("video_info")
|
||||
if video_info:
|
||||
# 停止播放
|
||||
video_info['playing'] = False
|
||||
|
||||
# 释放视频捕获资源
|
||||
if 'capture' in video_info and video_info['capture']:
|
||||
try:
|
||||
# 在单独的线程中释放资源,避免阻塞
|
||||
import threading
|
||||
def release_capture():
|
||||
try:
|
||||
video_info['capture'].release()
|
||||
except:
|
||||
pass
|
||||
|
||||
release_thread = threading.Thread(target=release_capture, daemon=True)
|
||||
release_thread.start()
|
||||
except:
|
||||
pass
|
||||
|
||||
# 清理纹理
|
||||
try:
|
||||
video_screen.clearTexture()
|
||||
except:
|
||||
pass
|
||||
|
||||
video_screen.clearPythonTag("video_info")
|
||||
|
||||
# 清理视频路径标签
|
||||
if video_screen.hasTag("video_path"):
|
||||
video_screen.clearTag("video_path")
|
||||
|
||||
print("⏹️ 视频已停止")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ 停止视频失败: {e}")
|
||||
return False
|
||||
def _loadNew2DVideo(self, video_screen):
|
||||
"""为2D视频屏幕加载新视频文件"""
|
||||
try:
|
||||
@ -1693,6 +2306,7 @@ class PropertyPanelManager:
|
||||
if success:
|
||||
print(f"成功加载新视频: {file_path}")
|
||||
# 刷新属性面板以显示新视频信息
|
||||
self._stopVideo(video_screen)
|
||||
self.updateGUIPropertyPanel(video_screen)
|
||||
return True
|
||||
except Exception as e:
|
||||
@ -1704,8 +2318,19 @@ class PropertyPanelManager:
|
||||
try:
|
||||
import os
|
||||
|
||||
# 处理空路径情况 - 显示空白
|
||||
if not video_path or video_path == "":
|
||||
print("ℹ️ 空视频路径,显示空白")
|
||||
video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景
|
||||
video_screen.clearPythonTag("movie_texture")
|
||||
video_screen.setTag("video_path", "")
|
||||
return True
|
||||
|
||||
# 检查文件是否存在
|
||||
if not os.path.exists(video_path):
|
||||
print(f"❌ 2D视频文件不存在: {video_path}")
|
||||
# 显示空白而不是红色错误提示
|
||||
video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景
|
||||
return False
|
||||
|
||||
# 加载新的视频纹理
|
||||
@ -1713,6 +2338,8 @@ class PropertyPanelManager:
|
||||
if movie_texture:
|
||||
# 应用纹理到2D视频屏幕
|
||||
video_screen["frameTexture"] = movie_texture
|
||||
# 设置白色背景以正确显示视频
|
||||
video_screen["frameColor"] = (1, 1, 1, 1)
|
||||
|
||||
# 保存视频纹理引用
|
||||
video_screen.setPythonTag("movie_texture", movie_texture)
|
||||
@ -1722,37 +2349,59 @@ class PropertyPanelManager:
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 无法加载2D视频文件: {video_path}")
|
||||
# 显示空白而不是红色错误提示
|
||||
video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 加载2D视频文件失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# 显示空白而不是红色错误提示
|
||||
video_screen["frameColor"] = (0, 0, 0, 0) # 透明背景
|
||||
return False
|
||||
|
||||
def _addVideoScreenProperties(self, video_screen):
|
||||
try:
|
||||
from PyQt5.QtWidgets import (QGroupBox,QGridLayout,QPushButton,QLabel,QSlider,QFileDialog,QHBoxLayout,QCheckBox)
|
||||
from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel,
|
||||
QFileDialog, QHBoxLayout, QLineEdit)
|
||||
from PyQt5.QtCore import Qt
|
||||
import os
|
||||
|
||||
video_info_group = QGroupBox("视频信息")
|
||||
video_info_layout = QGridLayout()
|
||||
|
||||
video_path = video_screen.getTag("video_path")
|
||||
if video_path and os.path.exists(video_path):
|
||||
video_info_layout.addWidget(QLabel("视频文件:"),0,0)
|
||||
path_label = QLabel(os.path.basename(video_path))
|
||||
# 显示当前视频文件路径 - 改进版本
|
||||
video_path = video_screen.getTag("video_path") if video_screen.hasTag("video_path") else ""
|
||||
if video_path:
|
||||
if video_path.startswith("http://") or video_path.startswith("https://"):
|
||||
video_info_layout.addWidget(QLabel("视频流URL:"),0,0)
|
||||
display_url = video_path if len(video_path) <= 50 else video_path[:47]+"..."
|
||||
path_label = QLabel(display_url)
|
||||
path_label.setWordWrap(True)
|
||||
path_label.setStyleSheet("color: #00AAFF;")
|
||||
video_info_layout.addWidget(path_label, 0, 1)
|
||||
elif os.path.exists(video_path):
|
||||
# 显示本地文件信息
|
||||
video_info_layout.addWidget(QLabel("视频文件:"), 0, 0)
|
||||
filename = os.path.basename(video_path)
|
||||
display_filename = filename if len(filename) <= 30 else filename[:27]+"..."
|
||||
path_label = QLabel(display_filename)
|
||||
path_label.setWordWrap(True)
|
||||
path_label.setStyleSheet("color: #00AAFF;")
|
||||
video_info_layout.addWidget(path_label, 0, 1)
|
||||
else:
|
||||
video_info_layout.addWidget(QLabel("状态:"),0,0)
|
||||
status_label = QLabel("无视频文件"if not video_path else "文件不存在")
|
||||
# 文件不存在
|
||||
video_info_layout.addWidget(QLabel("状态:"), 0, 0)
|
||||
status_label = QLabel("文件不存在")
|
||||
status_label.setStyleSheet("color: red;")
|
||||
video_info_layout.addWidget(status_label, 0, 1)
|
||||
else:
|
||||
# 无视频
|
||||
video_info_layout.addWidget(QLabel("状态:"), 0, 0)
|
||||
status_label = QLabel("无视频文件")
|
||||
status_label.setStyleSheet("color: red;")
|
||||
video_info_layout.addWidget(status_label, 0, 1)
|
||||
|
||||
video_info_group.setLayout(video_info_layout)
|
||||
self._propertyLayout.addWidget(video_info_group)
|
||||
|
||||
video_info_group.setLayout(video_info_layout)
|
||||
self._propertyLayout.addWidget(video_info_group)
|
||||
@ -1779,11 +2428,61 @@ class PropertyPanelManager:
|
||||
self._propertyLayout.addWidget(video_control_group)
|
||||
|
||||
# 加载新视频按钮
|
||||
load_btn = QPushButton("📁 加载新视频...")
|
||||
load_btn = QPushButton("📁 加载视频...")
|
||||
load_btn.clicked.connect(lambda: self._loadNewVideo(video_screen))
|
||||
self._propertyLayout.addWidget(load_btn)
|
||||
|
||||
# 添加URL输入区域 - 新增功能
|
||||
url_group = QGroupBox("网络视频")
|
||||
url_layout = QHBoxLayout()
|
||||
|
||||
self.url_input = QLineEdit()
|
||||
self.url_input.setPlaceholderText("输入视频流URL")
|
||||
# 如果已有URL,则预填充
|
||||
if video_path and (video_path.startswith("http://") or video_path.startswith("https://")):
|
||||
self.url_input.setText(video_path)
|
||||
|
||||
url_layout.addWidget(self.url_input)
|
||||
|
||||
load_url_btn = QPushButton("加载URL")
|
||||
# 修复:直接绑定处理函数,避免中间层
|
||||
load_url_btn.clicked.connect(lambda: self._directLoadURL(video_screen))
|
||||
url_layout.addWidget(load_url_btn)
|
||||
|
||||
url_group.setLayout(url_layout)
|
||||
self._propertyLayout.addWidget(url_group)
|
||||
|
||||
except Exception as e:
|
||||
print(f"添加视频屏幕属性失败{e}")
|
||||
print(f"添加视频屏幕属性失败: {e}")
|
||||
def _directLoadURL(self, video_screen):
|
||||
"""直接加载URL - 修复版"""
|
||||
try:
|
||||
# 获取URL文本
|
||||
url = self.url_input.text().strip() if hasattr(self, 'url_input') and self.url_input else ""
|
||||
|
||||
if not url:
|
||||
print("❌ URL不能为空")
|
||||
return False
|
||||
|
||||
print(f"🔍 开始加载视频URL: {url}")
|
||||
|
||||
# 停止当前播放的任何视频
|
||||
self._stopVideo(video_screen)
|
||||
|
||||
# 直接处理URL,不通过中间方法
|
||||
if url.startswith("http://") or url.startswith("https://"):
|
||||
# 网络流使用OpenCV方式 - 直接调用
|
||||
#return self._loadVideoFromURLWithOpenCV_3D_direct(video_screen, url)
|
||||
return self._loadVideoFromURLWithOpenCV(video_screen, url)
|
||||
else:
|
||||
# 本地文件使用MovieTexture方式
|
||||
return self.world.gui_manager.loadVideoFile(video_screen, url)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 直接加载URL失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def _loadNewVideo(self,video_screen):
|
||||
try:
|
||||
@ -2438,7 +3137,6 @@ class PropertyPanelManager:
|
||||
|
||||
return unique_names
|
||||
|
||||
|
||||
def _updateModelMaterialPanel(self,model):
|
||||
"""模型材质属性"""
|
||||
if model.is_empty():
|
||||
@ -2504,7 +3202,11 @@ class PropertyPanelManager:
|
||||
material_layout = QGridLayout()
|
||||
|
||||
material_layout.addWidget(QLabel("名称:"), 0, 0)
|
||||
name_label = QLabel(display_name)
|
||||
if len(display_name) > 25:
|
||||
limited_name = display_name[:22] + "..."
|
||||
else:
|
||||
limited_name = display_name
|
||||
name_label = QLabel(limited_name)
|
||||
# name_label.setStyleSheet("color: #FF6B6B; font-weight: bold;")
|
||||
material_layout.addWidget(name_label, 0, 1, 1, 3)
|
||||
|
||||
@ -2667,46 +3369,8 @@ class PropertyPanelManager:
|
||||
metallic_button.clicked.connect(lambda checked, mat=material: self._selectMetallicTexture(mat))
|
||||
material_layout.addWidget(metallic_button, current_row, 2, 1, 2)
|
||||
|
||||
# #IOR贴图
|
||||
# ior_button = QPushButton("选择IOR贴图")
|
||||
# ior_button.clicked.connect(lambda checked,mat = material:self._selectIORTexture(mat))
|
||||
# self._propertyLayout.addRow("IOR贴图",ior_button)
|
||||
|
||||
# # 视差贴图
|
||||
# parallax_button = QPushButton("选择视差贴图")
|
||||
# parallax_button.clicked.connect(lambda checked, mat=material: self._selectParallaxTexture(mat))
|
||||
# self._propertyLayout.addRow("视差贴图:", parallax_button)
|
||||
#
|
||||
# # 自发光贴图
|
||||
# emission_button = QPushButton("选择自发光贴图")
|
||||
# emission_button.clicked.connect(lambda checked, mat=material: self._selectEmissionTexture(mat))
|
||||
# self._propertyLayout.addRow("自发光贴图:", emission_button)
|
||||
#
|
||||
# # 环境光遮蔽贴图
|
||||
# ao_button = QPushButton("选择AO贴图")
|
||||
# ao_button.clicked.connect(lambda checked, mat=material: self._selectAOTexture(mat))
|
||||
# self._propertyLayout.addRow("AO贴图:", ao_button)
|
||||
|
||||
# # 透明度贴图
|
||||
# alpha_button = QPushButton("选择透明度贴图")
|
||||
# alpha_button.clicked.connect(lambda checked, mat=material: self._selectAlphaTexture(mat))
|
||||
# self._propertyLayout.addRow("透明度贴图:", alpha_button)
|
||||
#
|
||||
# # 细节贴图
|
||||
# detail_button = QPushButton("选择细节贴图")
|
||||
# detail_button.clicked.connect(lambda checked, mat=material: self._selectDetailTexture(mat))
|
||||
# self._propertyLayout.addRow("细节贴图:", detail_button)
|
||||
#
|
||||
# # 光泽贴图
|
||||
# gloss_button = QPushButton("选择光泽贴图")
|
||||
# gloss_button.clicked.connect(lambda checked, mat=material: self._selectGlossTexture(mat))
|
||||
# self._propertyLayout.addRow("光泽贴图:", gloss_button)
|
||||
|
||||
|
||||
|
||||
# 在纹理按钮后添加当前贴图信息显示
|
||||
current_row = self._displayCurrentTextures(material, material_layout, current_row)
|
||||
|
||||
current_row = self._addShadingModelPanel(material, material_layout, current_row)
|
||||
current_row = self._addEmissionPanel(material, material_layout, current_row)
|
||||
current_row = self._addMaterialPresetPanel(material, material_layout, current_row)
|
||||
@ -2714,17 +3378,6 @@ class PropertyPanelManager:
|
||||
material_group.setLayout(material_layout)
|
||||
self._propertyLayout.addWidget(material_group)
|
||||
|
||||
# # 添加太阳方位角控制面板(只在第一个材质时添加,避免重复)
|
||||
# # if i == 0:
|
||||
# # self._addSunAzimuthPanel()
|
||||
#
|
||||
#
|
||||
# # 分隔线
|
||||
# if i < len(materials) - 1:
|
||||
# separator = QLabel("─" * 30)
|
||||
# separator.setStyleSheet("color: lightgray;")
|
||||
# self._propertyLayout.addRow(separator)
|
||||
|
||||
def _updateMaterialBaseColor(self, material, component, value):
|
||||
"""更新材质基础颜色(智能版本)"""
|
||||
try:
|
||||
@ -5082,7 +5735,7 @@ class PropertyPanelManager:
|
||||
if preset["shading_model"]==3:
|
||||
self._apply_transparent_effect()
|
||||
|
||||
material._applied_preset = preset_name
|
||||
#material._applied_preset = preset_name
|
||||
self._refreshMaterialUI()
|
||||
print(f"已应用材质预设: {preset_name}")
|
||||
|
||||
@ -5114,38 +5767,6 @@ class PropertyPanelManager:
|
||||
# 触发属性面板更新
|
||||
self.updatePropertyPanel(current_item)
|
||||
|
||||
def _addColorSpacePanel(self, material):
|
||||
"""添加颜色空间选择面板"""
|
||||
from PyQt5.QtWidgets import QButtonGroup, QRadioButton, QHBoxLayout, QWidget
|
||||
|
||||
# color_space_title = QLabel("颜色空间")
|
||||
# color_space_title.setStyleSheet("color: #FF9800; font-weight:bold;")
|
||||
# self._propertyLayout.addRow(color_space_title)
|
||||
#
|
||||
# color_space_widget = QWidget()
|
||||
# color_space_layout = QHBoxLayout(color_space_widget)
|
||||
# color_space_group = QButtonGroup()
|
||||
|
||||
# rgb_radio = QRadioButton("RGB")
|
||||
# srgb_radio = QRadioButton("sRGB")
|
||||
# hsv_radio = QRadioButton("HSV")
|
||||
#
|
||||
# rgb_radio.setChecked(True)
|
||||
#
|
||||
# color_space_group.addButton(rgb_radio, 0)
|
||||
# color_space_group.addButton(srgb_radio, 1)
|
||||
# color_space_group.addButton(hsv_radio, 2)
|
||||
#
|
||||
# color_space_layout.addWidget(rgb_radio)
|
||||
# color_space_layout.addWidget(srgb_radio)
|
||||
# color_space_layout.addWidget(hsv_radio)
|
||||
#
|
||||
# color_space_group.buttonClicked.connect(
|
||||
# lambda button: self._updateColorSpace(material, color_space_group.id(button))
|
||||
# )
|
||||
#
|
||||
# self._propertyLayout.addRow("颜色空间:", color_space_widget)
|
||||
|
||||
def _addBatchOperationsPanel(self, model):
|
||||
"""添加批量操作面板"""
|
||||
batch_title = QLabel("批量操作")
|
||||
|
||||
@ -1445,28 +1445,21 @@ class CustomTreeWidget(QTreeWidget):
|
||||
event.ignore()
|
||||
return
|
||||
|
||||
# # 检查是否是有效的父子关系
|
||||
# if self.isValidParentChild(dragged_item, target_item):
|
||||
# # 保存当前的世界坐标
|
||||
# world_pos = dragged_node.getPos(self.world.render)
|
||||
#
|
||||
# # 更新场景图中的父子关系
|
||||
# dragged_node.wrtReparentTo(target_node)
|
||||
#
|
||||
# # 接受拖放事件,更新树形控件
|
||||
# super().dropEvent(event)
|
||||
#
|
||||
# #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item)
|
||||
# # 更新属性面板
|
||||
# self.world.updatePropertyPanel(dragged_item)
|
||||
# self.world.property_panel._syncEffectiveVisibility(dragged_node)
|
||||
|
||||
print(f"dragged_node: {dragged_node}, target_node: {target_node}")
|
||||
|
||||
# 记录拖拽前的父节点
|
||||
old_parent_item = dragged_item.parent()
|
||||
old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None
|
||||
|
||||
# 获取拖拽前的缩放值(2D GUI节点需要特别处理)
|
||||
is_2d_gui = dragged_item.data(0, Qt.UserRole + 1) in self.gui_2d_types
|
||||
if is_2d_gui:
|
||||
# 对于2D GUI,直接记录节点自身的缩放(不考虑父节点缩放)
|
||||
original_scale = dragged_node.getScale()
|
||||
else:
|
||||
# 对于3D节点,记录世界缩放
|
||||
original_scale = dragged_node.getScale(self.world.render)
|
||||
|
||||
# 执行Qt默认拖拽
|
||||
super().dropEvent(event)
|
||||
|
||||
@ -1483,11 +1476,6 @@ class CustomTreeWidget(QTreeWidget):
|
||||
# 保存世界坐标位置
|
||||
world_pos = dragged_node.getPos(self.world.render)
|
||||
world_hpr = dragged_node.getHpr(self.world.render)
|
||||
world_scale = dragged_node.getScale(self.world.render)
|
||||
|
||||
# 检查是否是2D GUI元素
|
||||
dragged_type = dragged_item.data(0, Qt.UserRole + 1)
|
||||
is_2d_gui = dragged_type in self.gui_2d_types
|
||||
|
||||
# 重新父化到新的父节点
|
||||
if new_parent_node:
|
||||
@ -1513,16 +1501,18 @@ class CustomTreeWidget(QTreeWidget):
|
||||
else:
|
||||
dragged_node.reparentTo(self.world.render)
|
||||
|
||||
# 恢复世界坐标位置(对于2D GUI可能需要调整)
|
||||
if is_2d_gui:
|
||||
# 2D GUI元素使用屏幕坐标系,可能需要特殊处理
|
||||
dragged_node.setPos(world_pos)
|
||||
dragged_node.setHpr(world_hpr)
|
||||
dragged_node.setScale(world_scale)
|
||||
else:
|
||||
# 恢复世界坐标位置和方向
|
||||
dragged_node.setPos(self.world.render, world_pos)
|
||||
dragged_node.setHpr(self.world.render, world_hpr)
|
||||
dragged_node.setScale(self.world.render, world_scale)
|
||||
|
||||
# 恢复缩放值
|
||||
if is_2d_gui:
|
||||
# 对于2D GUI,直接使用原始缩放值(不考虑父节点缩放)
|
||||
dragged_node.setScale(original_scale)
|
||||
print(f"✅ 2D GUI {dragged_item.text(0)} 缩放已恢复为原始值: {original_scale}")
|
||||
else:
|
||||
# 对于3D节点,使用世界缩放恢复
|
||||
dragged_node.setScale(self.world.render, original_scale)
|
||||
|
||||
print(f"✅ Panda3D父子关系已更新")
|
||||
else:
|
||||
@ -1537,28 +1527,6 @@ class CustomTreeWidget(QTreeWidget):
|
||||
self.world.property_panel._syncEffectiveVisibility(dragged_node)
|
||||
event.accept()
|
||||
|
||||
# try:
|
||||
# world_pos = dragged_node.getPos(self.world.render)
|
||||
#
|
||||
# parent_of_dragged = dragged_node.getParent()
|
||||
# target_node.wrtReparentTo(parent_of_dragged)
|
||||
#
|
||||
# # 拖动节点到目标节点下
|
||||
# dragged_node.wrtReparentTo(target_node)
|
||||
# dragged_node.setPos(self.world.render, world_pos)
|
||||
#
|
||||
# # 更新 Qt 树控件
|
||||
# super().dropEvent(event)
|
||||
#
|
||||
# # 更新属性面板
|
||||
# self.world.updatePropertyPanel(dragged_item)
|
||||
#
|
||||
# event.accept()
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f"重设父节点失败: {e}")
|
||||
# event.ignore()
|
||||
|
||||
def _ensureUnderSceneRoot(self, item):
|
||||
"""确保节点在场景根节点下,如果不是则自动修正"""
|
||||
if not item:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user