2839 lines
109 KiB
Python
2839 lines
109 KiB
Python
"""
|
||
自定义Qt部件模块
|
||
|
||
包含所有自定义的Qt界面组件:
|
||
- NewProjectDialog: 新建项目对话框
|
||
- CustomPanda3DWidget: 自定义Panda3D显示部件
|
||
- CustomFileView: 自定义文件浏览器
|
||
- CustomTreeWidget: 自定义场景树部件
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout,
|
||
QLineEdit, QPushButton, QLabel, QDialogButtonBox,
|
||
QTreeView, QTreeWidget, QTreeWidgetItem, QWidget,
|
||
QFileDialog, QMessageBox, QAbstractItemView, QMenu, QDockWidget, QButtonGroup, QToolButton)
|
||
from PyQt5.QtCore import Qt, QUrl, QMimeData
|
||
from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush
|
||
from PyQt5.sip import wrapinstance
|
||
from direct.showbase.ShowBaseGlobal import aspect2d
|
||
from panda3d.core import ModelRoot, NodePath
|
||
|
||
from QPanda3D.QPanda3DWidget import QPanda3DWidget
|
||
from scene import util
|
||
|
||
class NewProjectDialog(QDialog):
|
||
"""新建项目对话框"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("新建项目")
|
||
self.setMinimumWidth(500)
|
||
|
||
# 创建布局
|
||
layout = QVBoxLayout(self)
|
||
|
||
# 创建路径选择部分
|
||
pathGroup = QGroupBox("项目路径")
|
||
pathLayout = QHBoxLayout()
|
||
self.pathEdit = QLineEdit()
|
||
self.pathEdit.setReadOnly(True)
|
||
browseButton = QPushButton("浏览...")
|
||
browseButton.clicked.connect(self.browsePath)
|
||
pathLayout.addWidget(self.pathEdit)
|
||
pathLayout.addWidget(browseButton)
|
||
pathGroup.setLayout(pathLayout)
|
||
layout.addWidget(pathGroup)
|
||
|
||
# 创建项目名称部分
|
||
nameGroup = QGroupBox("项目名称")
|
||
nameLayout = QVBoxLayout()
|
||
self.nameEdit = QLineEdit()
|
||
self.nameEdit.setText("新项目")
|
||
self.nameEdit.selectAll()
|
||
nameLayout.addWidget(self.nameEdit)
|
||
# 添加提示标签
|
||
self.tipLabel = QLabel("项目名称只能包含字母、数字、下划线、中划线和中文")
|
||
self.tipLabel.setStyleSheet("color: gray;")
|
||
nameLayout.addWidget(self.tipLabel)
|
||
nameGroup.setLayout(nameLayout)
|
||
layout.addWidget(nameGroup)
|
||
|
||
# 添加按钮
|
||
buttonBox = QDialogButtonBox(
|
||
QDialogButtonBox.Ok | QDialogButtonBox.Cancel
|
||
)
|
||
buttonBox.accepted.connect(self.validate)
|
||
buttonBox.rejected.connect(self.reject)
|
||
layout.addWidget(buttonBox)
|
||
|
||
# 存储结果
|
||
self.projectPath = ""
|
||
self.projectName = ""
|
||
|
||
def browsePath(self):
|
||
"""浏览选择项目路径"""
|
||
path = QFileDialog.getExistingDirectory(self, "选择项目路径")
|
||
if path:
|
||
self.pathEdit.setText(path)
|
||
|
||
def validate(self):
|
||
"""验证输入并关闭对话框"""
|
||
# 获取并验证路径
|
||
self.projectPath = self.pathEdit.text()
|
||
if not self.projectPath:
|
||
QMessageBox.warning(self, "错误", "请选择项目路径!")
|
||
return
|
||
|
||
# 获取并验证项目名称
|
||
self.projectName = self.nameEdit.text().strip()
|
||
if not self.projectName:
|
||
QMessageBox.warning(self, "错误", "请输入项目名称!")
|
||
return
|
||
|
||
# 验证项目名称格式
|
||
if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName):
|
||
QMessageBox.warning(self, "错误",
|
||
"项目名称只能包含字母、数字、下划线、中划线和中文!")
|
||
return
|
||
|
||
# 检查项目是否已存在
|
||
full_path = os.path.join(self.projectPath, self.projectName)
|
||
if os.path.exists(full_path):
|
||
QMessageBox.warning(self, "错误", "项目已存在!")
|
||
return
|
||
|
||
self.accept()
|
||
|
||
class CustomPanda3DWidget(QPanda3DWidget):
|
||
"""自定义Panda3D显示部件"""
|
||
|
||
def __init__(self, world, parent=None):
|
||
if parent is None:
|
||
parent = wrapinstance(0, QWidget)
|
||
super().__init__(world, parent)
|
||
self.world = world
|
||
self.setFocusPolicy(Qt.StrongFocus) # 允许接收键盘焦点
|
||
self.setAcceptDrops(True) # 允许接收拖放
|
||
self.setMouseTracking(True) # 启用鼠标追踪
|
||
|
||
# 让world引用这个widget,以便获取准确的尺寸
|
||
if hasattr(world, 'setQtWidget'):
|
||
world.setQtWidget(self)
|
||
|
||
def getActualSize(self):
|
||
"""获取Qt部件的实际渲染尺寸"""
|
||
return (self.width(), self.height())
|
||
|
||
def dragEnterEvent(self, event):
|
||
"""处理拖拽进入事件"""
|
||
# 检查是否是文件拖拽
|
||
if event.mimeData().hasUrls():
|
||
# 检查是否包含支持的模型文件
|
||
for url in event.mimeData().urls():
|
||
filepath = url.toLocalFile()
|
||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
event.acceptProposedAction()
|
||
return
|
||
event.ignore()
|
||
|
||
def dragMoveEvent(self, event):
|
||
"""处理拖拽移动事件"""
|
||
if event.mimeData().hasUrls():
|
||
event.acceptProposedAction()
|
||
else:
|
||
event.ignore()
|
||
|
||
def dropEvent(self, event):
|
||
"""处理拖放事件"""
|
||
if event.mimeData().hasUrls():
|
||
for url in event.mimeData().urls():
|
||
filepath = url.toLocalFile()
|
||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
# 使用关键字参数确保兼容性
|
||
self.world.importModel(filepath)
|
||
event.acceptProposedAction()
|
||
else:
|
||
event.ignore()
|
||
|
||
def wheelEvent(self, event):
|
||
"""处理滚轮事件"""
|
||
if event.angleDelta().y() > 0:
|
||
# 滚轮向前滚动
|
||
self.world.wheelForward()
|
||
else:
|
||
# 滚轮向后滚动
|
||
self.world.wheelBackward()
|
||
event.accept()
|
||
|
||
def mousePressEvent(self, event):
|
||
"""处理 Qt 鼠标按下事件"""
|
||
if event.button() == Qt.LeftButton:
|
||
self.world.mousePressEventLeft({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
elif event.button() == Qt.RightButton:
|
||
self.world.mousePressEventRight({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
elif event.button() == Qt.MiddleButton: # 添加滑轮按下事件处理
|
||
self.world.mousePressEventMiddle({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
event.accept()
|
||
|
||
def mouseReleaseEvent(self, event):
|
||
"""处理 Qt 鼠标释放事件"""
|
||
if event.button() == Qt.LeftButton:
|
||
self.world.mouseReleaseEventLeft({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
elif event.button() == Qt.RightButton:
|
||
self.world.mouseReleaseEventRight({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
elif event.button() == Qt.MiddleButton: # 添加滑轮释放事件处理
|
||
self.world.mouseReleaseEventMiddle({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
event.accept()
|
||
|
||
def mouseMoveEvent(self, event):
|
||
"""处理 Qt 鼠标移动事件"""
|
||
self.world.mouseMoveEvent({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
event.accept()
|
||
|
||
def cleanup(self):
|
||
"""清理Panda3D资源"""
|
||
try:
|
||
print("🧹 清理CustomPanda3DWidget资源...")
|
||
|
||
# 清理world资源
|
||
if hasattr(self, 'world') and self.world:
|
||
# 如果world有cleanup方法,调用它
|
||
if hasattr(self.world, 'cleanup'):
|
||
self.world.cleanup()
|
||
# 清理world引用
|
||
self.world = None
|
||
|
||
# 调用父类的清理方法(如果存在)
|
||
if hasattr(super(), 'cleanup'):
|
||
super().cleanup()
|
||
|
||
print("✅ CustomPanda3DWidget资源清理完成")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 清理CustomPanda3DWidget资源时出错: {e}")
|
||
|
||
class CustomFileView(QTreeView):
|
||
"""自定义文件浏览器"""
|
||
|
||
def __init__(self, world, parent=None):
|
||
if parent is None:
|
||
parent = wrapinstance(0, QWidget)
|
||
super().__init__(parent)
|
||
self.world = world
|
||
self.setupUI()
|
||
self.setupDragDrop()
|
||
|
||
def setupUI(self):
|
||
"""初始化UI设置"""
|
||
self.setHeaderHidden(True)
|
||
# 启用多选和拖拽
|
||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||
self.setDropIndicatorShown(True) # 启用拖放指示线
|
||
|
||
def setupDragDrop(self):
|
||
"""设置拖拽功能"""
|
||
# 使用自定义拖拽模式
|
||
self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入
|
||
self.setDefaultDropAction(Qt.DropAction.MoveAction)
|
||
self.setDragEnabled(True)
|
||
self.setAcceptDrops(True)
|
||
|
||
def startDrag(self, supportedActions):
|
||
"""开始拖拽操作"""
|
||
# 获取选中的文件
|
||
indexes = self.selectedIndexes()
|
||
if not indexes:
|
||
return
|
||
|
||
# 只处理文件名列
|
||
indexes = [idx for idx in indexes if idx.column() == 0]
|
||
|
||
# 创建 MIME 数据
|
||
mimeData = self.model().mimeData(indexes)
|
||
|
||
# 检查是否包含支持的模型文件
|
||
urls = []
|
||
for index in indexes:
|
||
filepath = self.model().filePath(index)
|
||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
urls.append(QUrl.fromLocalFile(filepath))
|
||
|
||
if not urls:
|
||
return
|
||
|
||
# 设置 URL 列表
|
||
mimeData.setUrls(urls)
|
||
|
||
# 创建拖拽对象
|
||
drag = QDrag(self)
|
||
drag.setMimeData(mimeData)
|
||
|
||
# 设置拖拽图标(可选)
|
||
pixmap = QPixmap(32, 32)
|
||
pixmap.fill(Qt.transparent)
|
||
painter = QPainter(pixmap)
|
||
painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(urls)))
|
||
painter.end()
|
||
drag.setPixmap(pixmap)
|
||
|
||
# 执行拖拽
|
||
drag.exec_(supportedActions)
|
||
|
||
def mouseDoubleClickEvent(self, event):
|
||
"""处理双击事件"""
|
||
index = self.indexAt(event.pos())
|
||
if index.isValid():
|
||
model = self.model()
|
||
filepath = model.filePath(index)
|
||
|
||
# 检查是否是模型文件
|
||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
self.world.importModel(filepath)
|
||
else:
|
||
print("不支持的文件类型")
|
||
super().mouseDoubleClickEvent(event)
|
||
|
||
from PyQt5.QtCore import QFileSystemWatcher,QTimer
|
||
import os
|
||
|
||
class CustomAssetsTreeWidget(QTreeWidget):
|
||
def __init__(self, world, parent=None):
|
||
if parent is None:
|
||
parent = wrapinstance(0, QWidget)
|
||
super().__init__(parent)
|
||
self.world = world
|
||
self.root_path = None
|
||
self.setupUI()
|
||
self.setupDragDrop()
|
||
|
||
#添加文件系统监控器
|
||
self.file_watcher = QFileSystemWatcher()
|
||
self.file_watcher.directoryChanged.connect(self.onDirectoryChanged)
|
||
self.file_watcher.fileChanged.connect(self.onFileChanged)
|
||
|
||
self.refresh_timer = QTimer()
|
||
self.refresh_timer.setSingleShot(True)
|
||
self.refresh_timer.timeout.connect(self.refreshView)
|
||
|
||
#存储监控的目录
|
||
self.watched_directories = set()
|
||
|
||
# 默认加载项目根路径
|
||
self.load_file_tree()
|
||
# 设置右键菜单
|
||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||
self.customContextMenuRequested.connect(self.showContextMenu)
|
||
|
||
def showContextMenu(self, position):
|
||
"""显示右键菜单"""
|
||
item = self.itemAt(position)
|
||
if not item:
|
||
return
|
||
|
||
filepath = item.data(0, Qt.UserRole)
|
||
is_folder = item.data(0, Qt.UserRole + 1)
|
||
|
||
menu = QMenu(self)
|
||
|
||
if is_folder:
|
||
# 文件夹右键菜单
|
||
menu.addAction("📁 新建文件夹", lambda: self.createNewFolder(item))
|
||
menu.addAction("📄 新建文件", lambda: self.createNewFile(item))
|
||
menu.addSeparator()
|
||
menu.addAction("✏️ 重命名", lambda: self.renameItem(item))
|
||
menu.addAction("🗑️ 删除", lambda: self.deleteItem(item))
|
||
menu.addSeparator()
|
||
menu.addAction("📋 复制路径", lambda: self.copyPath(filepath))
|
||
menu.addAction("🔍 查看属性", lambda: self.showProperties(item))
|
||
else:
|
||
# 文件右键菜单
|
||
menu.addAction("📂 打开文件", lambda: self.openFile(filepath))
|
||
menu.addSeparator()
|
||
menu.addAction("✏️ 重命名", lambda: self.renameItem(item))
|
||
menu.addAction("🗑️ 删除", lambda: self.deleteItem(item))
|
||
menu.addSeparator()
|
||
menu.addAction("📋 复制路径", lambda: self.copyPath(filepath))
|
||
menu.addAction("🔍 查看属性", lambda: self.showProperties(item))
|
||
|
||
# 显示菜单
|
||
menu.exec_(self.mapToGlobal(position))
|
||
|
||
def _saveExpandedState(self):
|
||
"""保存展开状态"""
|
||
expanded_paths = set()
|
||
|
||
def collectExpanded(item):
|
||
if item.isExpanded():
|
||
path = item.data(0, Qt.UserRole)
|
||
if path:
|
||
expanded_paths.add(path)
|
||
|
||
for i in range(item.childCount()):
|
||
collectExpanded(item.child(i))
|
||
|
||
# 遍历所有顶级项目
|
||
for i in range(self.topLevelItemCount()):
|
||
collectExpanded(self.topLevelItem(i))
|
||
|
||
return expanded_paths
|
||
|
||
def _restoreExpandedState(self, expanded_paths):
|
||
"""恢复展开状态"""
|
||
def restoreExpanded(item):
|
||
path = item.data(0, Qt.UserRole)
|
||
if path in expanded_paths:
|
||
item.setExpanded(True)
|
||
|
||
for i in range(item.childCount()):
|
||
restoreExpanded(item.child(i))
|
||
|
||
# 遍历所有顶级项目
|
||
for i in range(self.topLevelItemCount()):
|
||
restoreExpanded(self.topLevelItem(i))
|
||
|
||
def _refreshWithStatePreservation(self):
|
||
"""刷新树形视图并保持状态"""
|
||
# 保存当前状态
|
||
expanded_paths = self._saveExpandedState()
|
||
|
||
# 刷新树形结构
|
||
self.load_file_tree()
|
||
|
||
# 恢复展开状态
|
||
self._restoreExpandedState(expanded_paths)
|
||
|
||
def createNewFolder(self, parent_item):
|
||
"""创建新文件夹"""
|
||
import os
|
||
from PyQt5.QtWidgets import QInputDialog
|
||
|
||
parent_path = parent_item.data(0, Qt.UserRole)
|
||
folder_name, ok = QInputDialog.getText(self, "新建文件夹", "文件夹名称:")
|
||
|
||
if ok and folder_name:
|
||
new_folder_path = os.path.join(parent_path, folder_name)
|
||
try:
|
||
os.makedirs(new_folder_path, exist_ok=True)
|
||
self._refreshWithStatePreservation()
|
||
print(f"创建文件夹: {new_folder_path}")
|
||
except OSError as e:
|
||
print(f"创建文件夹失败: {e}")
|
||
|
||
def createNewFile(self, parent_item):
|
||
"""创建新文件"""
|
||
import os
|
||
from PyQt5.QtWidgets import QInputDialog
|
||
|
||
parent_path = parent_item.data(0, Qt.UserRole)
|
||
file_name, ok = QInputDialog.getText(self, "新建文件", "文件名称:")
|
||
|
||
if ok and file_name:
|
||
new_file_path = os.path.join(parent_path, file_name)
|
||
try:
|
||
with open(new_file_path, 'w', encoding='utf-8') as f:
|
||
f.write("")
|
||
self._refreshWithStatePreservation()
|
||
print(f"创建文件: {new_file_path}")
|
||
except OSError as e:
|
||
print(f"创建文件失败: {e}")
|
||
|
||
def renameItem(self, item):
|
||
"""重命名文件或文件夹"""
|
||
import os
|
||
from PyQt5.QtWidgets import QInputDialog
|
||
|
||
old_path = item.data(0, Qt.UserRole)
|
||
old_name = os.path.basename(old_path)
|
||
|
||
new_name, ok = QInputDialog.getText(self, "重命名", "新名称:", text=old_name)
|
||
|
||
if ok and new_name and new_name != old_name:
|
||
parent_dir = os.path.dirname(old_path)
|
||
new_path = os.path.join(parent_dir, new_name)
|
||
|
||
try:
|
||
os.rename(old_path, new_path)
|
||
self._refreshWithStatePreservation()
|
||
print(f"重命名: {old_path} -> {new_path}")
|
||
except OSError as e:
|
||
print(f"重命名失败: {e}")
|
||
|
||
def deleteItem(self, item):
|
||
"""删除文件或文件夹"""
|
||
import os
|
||
import shutil
|
||
from PyQt5.QtWidgets import QMessageBox
|
||
|
||
filepath = item.data(0, Qt.UserRole)
|
||
is_folder = item.data(0, Qt.UserRole + 1)
|
||
|
||
item_type = "文件夹" if is_folder else "文件"
|
||
reply = QMessageBox.question(
|
||
self,
|
||
"确认删除",
|
||
f"确定要删除这个{item_type}吗?\n{filepath}",
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No
|
||
)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
try:
|
||
if is_folder:
|
||
shutil.rmtree(filepath)
|
||
else:
|
||
os.remove(filepath)
|
||
self._refreshWithStatePreservation()
|
||
print(f"删除{item_type}: {filepath}")
|
||
except OSError as e:
|
||
print(f"删除{item_type}失败: {e}")
|
||
|
||
def copyPath(self, filepath):
|
||
"""复制路径到剪贴板"""
|
||
from PyQt5.QtWidgets import QApplication
|
||
|
||
clipboard = QApplication.clipboard()
|
||
clipboard.setText(filepath)
|
||
print(f"已复制路径: {filepath}")
|
||
|
||
def openFile(self, filepath):
|
||
"""打开文件"""
|
||
import os
|
||
import subprocess
|
||
import platform
|
||
|
||
try:
|
||
system = platform.system()
|
||
if system == "Windows":
|
||
os.startfile(filepath)
|
||
elif system == "Darwin": # macOS
|
||
subprocess.run(["open", filepath])
|
||
else: # Linux
|
||
subprocess.run(["xdg-open", filepath])
|
||
print(f"打开文件: {filepath}")
|
||
except Exception as e:
|
||
print(f"打开文件失败: {e}")
|
||
|
||
def showProperties(self, item):
|
||
"""显示属性面板"""
|
||
import os
|
||
from PyQt5.QtWidgets import QMessageBox
|
||
|
||
filepath = item.data(0, Qt.UserRole)
|
||
is_folder = item.data(0, Qt.UserRole + 1)
|
||
|
||
try:
|
||
stat = os.stat(filepath)
|
||
size = stat.st_size
|
||
modified = os.path.getmtime(filepath)
|
||
|
||
import datetime
|
||
modified_str = datetime.datetime.fromtimestamp(modified).strftime('%Y-%m-%d %H:%M:%S')
|
||
|
||
item_type = "文件夹" if is_folder else "文件"
|
||
size_str = f"{size} 字节" if not is_folder else "文件夹"
|
||
|
||
properties = f"""
|
||
路径: {filepath}
|
||
类型: {item_type}
|
||
大小: {size_str}
|
||
修改时间: {modified_str}
|
||
"""
|
||
|
||
QMessageBox.information(self, "属性", properties.strip())
|
||
|
||
except OSError as e:
|
||
QMessageBox.warning(self, "错误", f"无法获取属性: {e}")
|
||
|
||
# def mouseDoubleClickEvent(self, event):
|
||
# """处理双击事件"""
|
||
# item = self.itemAt(event.pos())
|
||
# if item:
|
||
# filepath = item.data(0, Qt.UserRole)
|
||
# is_folder = item.data(0, Qt.UserRole + 1)
|
||
#
|
||
# if is_folder:
|
||
# # 文件夹:展开/折叠
|
||
# item.setExpanded(not item.isExpanded())
|
||
# else:
|
||
# # 文件:检查是否是模型文件
|
||
# if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
# self.world.importModel(filepath)
|
||
# else:
|
||
# # 其他文件:用系统默认程序打开
|
||
# self.openFile(filepath)
|
||
#
|
||
# super().mouseDoubleClickEvent(event)
|
||
|
||
def setupUI(self):
|
||
"""初始化UI设置"""
|
||
self.setHeaderHidden(True)
|
||
# 启用多选和拖拽
|
||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||
self.setDropIndicatorShown(True) # 启用拖放指示线
|
||
|
||
def setupDragDrop(self):
|
||
"""设置拖拽功能"""
|
||
# 使用InternalMove模式以正确显示插入指示线
|
||
self.setDragDropMode(QAbstractItemView.InternalMove)
|
||
self.setDefaultDropAction(Qt.DropAction.MoveAction)
|
||
self.setDragEnabled(True)
|
||
self.setAcceptDrops(True)
|
||
self.setDropIndicatorShown(True)
|
||
|
||
def getProjectRootPath(self):
|
||
"""获取项目根路径下的Resources文件夹,考虑跨平台"""
|
||
import os
|
||
|
||
# 获取当前文件所在目录,然后向上查找项目根目录
|
||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
# 向上查找直到找到项目根目录(包含特定标识文件或文件夹)
|
||
project_root = current_dir
|
||
max_depth = 10 # 限制向上查找的深度
|
||
depth = 0
|
||
|
||
while depth < max_depth:
|
||
# 检查是否是项目根目录(可以根据实际情况调整判断条件)
|
||
if (os.path.exists(os.path.join(project_root, "main.py")) or
|
||
os.path.exists(os.path.join(project_root, "setup.py")) or
|
||
os.path.exists(os.path.join(project_root, ".git"))):
|
||
break
|
||
parent_dir = os.path.dirname(project_root)
|
||
if parent_dir == project_root: # 已经到达文件系统根目录
|
||
# 回退到使用当前工作目录
|
||
project_root = os.getcwd()
|
||
break
|
||
project_root = parent_dir
|
||
depth += 1
|
||
|
||
# 构建Resources文件夹路径(跨平台)
|
||
resources_path = os.path.join(project_root, "Resources")
|
||
|
||
# 如果Resources文件夹不存在,创建它
|
||
if not os.path.exists(resources_path):
|
||
try:
|
||
os.makedirs(resources_path, exist_ok=True)
|
||
print(f"创建Resources文件夹: {resources_path}")
|
||
except OSError as e:
|
||
print(f"无法创建Resources文件夹: {e}")
|
||
# 如果无法创建,回退到项目根路径
|
||
return project_root
|
||
|
||
return resources_path
|
||
|
||
# def getProjectRootPath(self):
|
||
# """获取项目根路径下的Resources文件夹,考虑跨平台"""
|
||
# import os
|
||
#
|
||
# # 获取项目根路径
|
||
# project_root = os.getcwd()
|
||
#
|
||
# # 构建Resources文件夹路径(跨平台)
|
||
# resources_path = os.path.join(project_root, "Resources")
|
||
#
|
||
# # 如果Resources文件夹不存在,创建它
|
||
# if not os.path.exists(resources_path):
|
||
# try:
|
||
# os.makedirs(resources_path, exist_ok=True)
|
||
# print(f"创建Resources文件夹: {resources_path}")
|
||
# except OSError as e:
|
||
# print(f"无法创建Resources文件夹: {e}")
|
||
# # 如果无法创建,回退到项目根路径
|
||
# return project_root
|
||
#
|
||
# return resources_path
|
||
|
||
def load_file_tree(self):
|
||
"""加载树形视图"""
|
||
self.clear()
|
||
self.current_path = self.getProjectRootPath()
|
||
try:
|
||
# 创建当前目录的根节点
|
||
root_name = os.path.basename(self.current_path) or self.current_path
|
||
root_item = QTreeWidgetItem([f"📁 {root_name}"])
|
||
root_item.setData(0, Qt.UserRole, self.current_path)
|
||
root_item.setData(0, Qt.UserRole + 1, True)
|
||
self.addTopLevelItem(root_item)
|
||
|
||
# 加载当前目录内容
|
||
self.load_directory_tree(self.current_path, root_item)
|
||
|
||
#添加目录到监控器
|
||
self.addWatchedDirectory(self.current_path)
|
||
|
||
# 展开根节点
|
||
root_item.setExpanded(True)
|
||
|
||
except PermissionError:
|
||
error_item = QTreeWidgetItem(["❌ 无权限访问此目录"])
|
||
self.addTopLevelItem(error_item)
|
||
|
||
def addWatchedDirectory(self,directory):
|
||
"""添加监控目录"""
|
||
if os.path.exists(directory) and directory not in self.watched_directories:
|
||
if self.file_watcher.addPath(directory):
|
||
self.watched_directories.add(directory)
|
||
#print(f"开始监控目录:{directory}")
|
||
try:
|
||
for item in os.listdir(directory):
|
||
item_path = os.path.join(directory,item)
|
||
if os.path.isdir(item_path):
|
||
self.addWatchedDirectory(item_path)
|
||
except Exception as e:
|
||
pass
|
||
else:
|
||
print(f"无法监控目录:{directory}")
|
||
|
||
def removeWatchedDirectory(self,directory):
|
||
"""移除监控目录"""
|
||
if directory in self.watched_directories:
|
||
if self.file_watcher.removePath(directory):
|
||
self.watched_directories.discard(directory)
|
||
print(f"停止监控目录:{directory}")
|
||
else:
|
||
print(f"无法停止监控目录:{directory}")
|
||
|
||
def onDirectoryChanged(self,path):
|
||
"""目录发生变化时处理"""
|
||
print(f"目录变化{path}")
|
||
if not self.refresh_timer.isActive():
|
||
self.refresh_timer.start(1000)
|
||
|
||
def onFileChanged(self,path):
|
||
"""目录发生变化时的处理"""
|
||
print(f"目录变化{path}")
|
||
if not self.refresh_timer.isActive():
|
||
self.refresh_timer.start(1000)
|
||
|
||
def refreshView(self):
|
||
"""刷新视图"""
|
||
print("刷新资源视图...")
|
||
try:
|
||
expanded_paths = self._saveExpandedState()
|
||
self.load_file_tree()
|
||
self._restoreExpandedState(expanded_paths)
|
||
print("资源视图刷新完成")
|
||
except Exception as e:
|
||
print(f"刷新资源视图失败{e}")
|
||
|
||
def load_directory_tree(self, path, parent_item, max_depth=3, current_depth=0):
|
||
"""递归加载目录树(类似左侧导航面板)"""
|
||
if current_depth >= max_depth:
|
||
return
|
||
|
||
try:
|
||
items = os.listdir(path)
|
||
items.sort()
|
||
|
||
# 分别处理文件夹和文件
|
||
folders = []
|
||
files = []
|
||
|
||
for item in items:
|
||
# 跳过隐藏文件和系统文件
|
||
if item.startswith('.') or item.startswith('__'):
|
||
continue
|
||
|
||
item_path = os.path.join(path, item)
|
||
if os.path.isdir(item_path):
|
||
folders.append(item)
|
||
elif os.path.isfile(item_path):
|
||
files.append(item)
|
||
|
||
# 先添加文件夹
|
||
for folder in folders:
|
||
folder_path = os.path.join(path, folder)
|
||
folder_item = self.create_simple_tree_item(folder, folder_path, True)
|
||
parent_item.addChild(folder_item)
|
||
|
||
# 递归加载子目录(限制深度)
|
||
if current_depth < max_depth - 1:
|
||
self.load_directory_tree(folder_path, folder_item, max_depth, current_depth + 1)
|
||
|
||
# 再添加文件(显示重要文件类型,包括图片和模型)
|
||
important_extensions = {
|
||
# 编程文件
|
||
'.py', '.js', '.html', '.css', '.java', '.cpp', '.c', '.php', '.rb', '.go', '.rs',
|
||
# 文档文件
|
||
'.md', '.txt', '.pdf', '.doc', '.docx', '.rtf',
|
||
# 配置和数据文件
|
||
'.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.toml',
|
||
# 图片文件
|
||
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif',
|
||
# 3D模型文件
|
||
'.fbx', '.obj', '.3ds', '.max', '.blend', '.dae', '.gltf', '.glb', '.stl', '.ply',
|
||
# 音视频文件
|
||
'.mp3', '.wav', '.mp4', '.avi', '.mov', '.wmv', '.flac', '.aac',
|
||
# 压缩文件
|
||
'.zip', '.rar', '.7z', '.tar', '.gz',
|
||
# 材质和纹理
|
||
'.mtl', '.mat', '.exr', '.hdr', '.hdri', '.dds',
|
||
# CAD文件
|
||
'.dwg', '.dxf', '.step', '.stp', '.iges', '.sldprt', '.sldasm',
|
||
# 其他重要文件
|
||
'.exe', '.dll', '.bat', '.sh', '.log'
|
||
}
|
||
|
||
# 重要文件名(不依赖扩展名)
|
||
important_files = {'requirements.txt', 'README.md', 'main.py', 'LICENSE', 'CHANGELOG.md', 'INSTALL.md'}
|
||
|
||
for file in files:
|
||
file_ext = os.path.splitext(file)[1].lower()
|
||
if file_ext in important_extensions or file in important_files:
|
||
file_path = os.path.join(path, file)
|
||
file_item = self.create_simple_tree_item(file, file_path, False)
|
||
parent_item.addChild(file_item)
|
||
|
||
except (OSError, PermissionError):
|
||
pass
|
||
|
||
def create_simple_tree_item(self, name, path, is_folder):
|
||
"""创建简单的树形项目"""
|
||
try:
|
||
if is_folder:
|
||
# 文件夹项目,展开/折叠图标由CSS样式控制
|
||
item = QTreeWidgetItem([f"📁 {name}"])
|
||
item.setData(0, Qt.UserRole + 1, True) # 标记为文件夹
|
||
else:
|
||
icon = self.get_file_icon(name)
|
||
item = QTreeWidgetItem([f"{icon} {name}"])
|
||
item.setData(0, Qt.UserRole + 1, False) # 标记为文件
|
||
|
||
item.setData(0, Qt.UserRole, path)
|
||
|
||
return item
|
||
|
||
except (OSError, PermissionError):
|
||
item = QTreeWidgetItem([name])
|
||
item.setData(0, Qt.UserRole, path)
|
||
return item
|
||
|
||
def get_file_icon(self, filename):
|
||
"""
|
||
根据文件扩展名获取图标
|
||
|
||
这个函数根据文件的扩展名返回对应的Unicode图标字符。
|
||
如果文件类型不在映射表中,返回默认的文档图标。
|
||
|
||
参数:
|
||
filename (str): 文件名(包含扩展名)
|
||
|
||
返回值:
|
||
str: 对应的Unicode图标字符
|
||
"""
|
||
# 获取文件扩展名并转换为小写
|
||
ext = os.path.splitext(filename)[1].lower()
|
||
|
||
# 文件扩展名到图标的映射表
|
||
icon_map = {
|
||
# 编程语言文件
|
||
'.py': '🐍', # Python文件
|
||
'.js': '⚡', # JavaScript文件
|
||
'.html': '🌐', # HTML文件
|
||
'.css': '🎨', # CSS样式文件
|
||
'.java': '☕', # Java文件
|
||
'.cpp': '⚙️', # C++文件
|
||
'.c': '⚙️', # C文件
|
||
'.h': '📋', # 头文件
|
||
'.php': '🐘', # PHP文件
|
||
'.rb': '💎', # Ruby文件
|
||
'.go': '🐹', # Go文件
|
||
'.rs': '🦀', # Rust文件
|
||
'.swift': '🐦', # Swift文件
|
||
'.kt': '🎯', # Kotlin文件
|
||
|
||
# 文档文件
|
||
'.txt': '📄', # 纯文本文件
|
||
'.md': '📝', # Markdown文档
|
||
'.rst': '📝', # reStructuredText文档
|
||
'.pdf': '📕', # PDF文档
|
||
'.doc': '📘', # Word文档
|
||
'.docx': '📘', # Word文档
|
||
'.rtf': '📄', # RTF文档
|
||
'.odt': '📄', # OpenDocument文本
|
||
|
||
# 数据文件
|
||
'.json': '📋', # JSON数据文件
|
||
'.xml': '📋', # XML文件
|
||
'.yaml': '📋', # YAML文件
|
||
'.yml': '📋', # YAML文件
|
||
'.csv': '📊', # CSV表格文件
|
||
'.xls': '📗', # Excel表格
|
||
'.xlsx': '📗', # Excel表格
|
||
'.ods': '📊', # OpenDocument表格
|
||
|
||
# 图像文件
|
||
'.jpg': '🖼️', # JPEG图像
|
||
'.jpeg': '🖼️', # JPEG图像
|
||
'.png': '🖼️', # PNG图像
|
||
'.gif': '🖼️', # GIF图像
|
||
'.bmp': '🖼️', # BMP图像
|
||
'.svg': '🎨', # SVG矢量图
|
||
'.ico': '🎯', # 图标文件
|
||
'.webp': '🖼️', # WebP图像
|
||
'.tiff': '🖼️', # TIFF图像
|
||
'.tif': '🖼️', # TIFF图像
|
||
|
||
# 音视频文件
|
||
'.mp4': '🎬', # MP4视频
|
||
'.avi': '🎬', # AVI视频
|
||
'.mov': '🎬', # MOV视频
|
||
'.wmv': '🎬', # WMV视频
|
||
'.flv': '🎬', # FLV视频
|
||
'.webm': '🎬', # WebM视频
|
||
'.mp3': '🎵', # MP3音频
|
||
'.wav': '🎵', # WAV音频
|
||
'.flac': '🎵', # FLAC音频
|
||
'.aac': '🎵', # AAC音频
|
||
'.ogg': '🎵', # OGG音频
|
||
|
||
# 压缩文件
|
||
'.zip': '📦', # ZIP压缩包
|
||
'.rar': '📦', # RAR压缩包
|
||
'.7z': '📦', # 7Z压缩包
|
||
'.tar': '📦', # TAR归档
|
||
'.gz': '📦', # GZIP压缩
|
||
'.bz2': '📦', # BZIP2压缩
|
||
'.xz': '📦', # XZ压缩
|
||
|
||
# 可执行文件
|
||
'.exe': '⚙️', # Windows可执行文件
|
||
'.msi': '📦', # Windows安装包
|
||
'.deb': '📦', # Debian包
|
||
'.rpm': '📦', # RPM包
|
||
'.dmg': '💿', # macOS磁盘镜像
|
||
'.app': '📱', # macOS应用程序
|
||
|
||
# 系统文件
|
||
'.dll': '🔧', # 动态链接库
|
||
'.so': '🔧', # 共享库
|
||
'.dylib': '🔧', # macOS动态库
|
||
'.lib': '📚', # 静态库
|
||
|
||
# 脚本文件
|
||
'.bat': '📜', # Windows批处理
|
||
'.cmd': '📜', # Windows命令脚本
|
||
'.sh': '📜', # Shell脚本
|
||
'.ps1': '💙', # PowerShell脚本
|
||
'.vbs': '📜', # VBScript脚本
|
||
|
||
# 配置文件
|
||
'.ini': '⚙️', # INI配置文件
|
||
'.cfg': '⚙️', # 配置文件
|
||
'.conf': '⚙️', # 配置文件
|
||
'.config': '⚙️', # 配置文件
|
||
'.toml': '⚙️', # TOML配置文件
|
||
|
||
# 3D模型文件
|
||
'.fbx': '🎭', # FBX模型文件
|
||
'.obj': '🎭', # OBJ模型文件
|
||
'.3ds': '🎭', # 3DS Max模型
|
||
'.max': '🎭', # 3DS Max场景
|
||
'.blend': '🎭', # Blender模型
|
||
'.dae': '🎭', # COLLADA模型
|
||
'.gltf': '🎭', # glTF模型
|
||
'.glb': '🎭', # glTF二进制模型
|
||
'.x3d': '🎭', # X3D模型
|
||
'.ply': '🎭', # PLY模型
|
||
'.stl': '🎭', # STL模型(3D打印)
|
||
'.off': '🎭', # OFF模型
|
||
'.3mf': '🎭', # 3MF模型
|
||
'.amf': '🎭', # AMF模型
|
||
'.x': '🎭', # DirectX模型
|
||
'.md2': '🎭', # Quake II模型
|
||
'.md3': '🎭', # Quake III模型
|
||
'.mdl': '🎭', # Source引擎模型
|
||
'.mesh': '🎭', # OGRE模型
|
||
'.scene': '🎭', # OGRE场景
|
||
'.ac': '🎭', # AC3D模型
|
||
'.ase': '🎭', # ASCII Scene Export
|
||
'.assbin': '🎭', # Assimp二进制
|
||
'.b3d': '🎭', # Blitz3D模型
|
||
'.bvh': '🎭', # BioVision层次
|
||
'.csm': '🎭', # CharacterStudio Motion
|
||
'.hmp': '🎭', # 3D GameStudio模型
|
||
'.irr': '🎭', # Irrlicht场景
|
||
'.irrmesh': '🎭', # Irrlicht网格
|
||
'.lwo': '🎭', # LightWave对象
|
||
'.lws': '🎭', # LightWave场景
|
||
'.ms3d': '🎭', # MilkShape 3D
|
||
'.nff': '🎭', # Neutral文件格式
|
||
'.q3o': '🎭', # Quick3D对象
|
||
'.q3s': '🎭', # Quick3D场景
|
||
'.raw': '🎭', # RAW三角形
|
||
'.smd': '🎭', # Valve SMD
|
||
'.ter': '🎭', # Terragen地形
|
||
'.uc': '🎭', # Unreal模型
|
||
'.vta': '🎭', # Valve VTA
|
||
'.xgl': '🎭', # XGL模型
|
||
'.zgl': '🎭', # ZGL模型
|
||
|
||
# 纹理和材质文件
|
||
'.mtl': '🎨', # OBJ材质文件
|
||
'.mat': '🎨', # 材质文件
|
||
'.sbsar': '🎨', # Substance Archive
|
||
'.sbs': '🎨', # Substance Designer
|
||
'.sbsm': '🎨', # Substance材质
|
||
'.exr': '🖼️', # OpenEXR高动态范围图像
|
||
'.hdr': '🖼️', # HDR图像
|
||
'.hdri': '🖼️', # HDRI环境贴图
|
||
'.dds': '🖼️', # DirectDraw Surface
|
||
'.ktx': '🖼️', # Khronos纹理
|
||
'.astc': '🖼️', # ASTC纹理
|
||
'.pvr': '🖼️', # PowerVR纹理
|
||
'.etc1': '🖼️', # ETC1纹理
|
||
'.etc2': '🖼️', # ETC2纹理
|
||
|
||
# 动画文件
|
||
'.anim': '🎬', # 动画文件
|
||
'.fbx': '🎬', # FBX动画(也可以是模型)
|
||
'.bip': '🎬', # Character Studio Biped
|
||
'.cal3d': '🎬', # Cal3D动画
|
||
'.motion': '🎬', # 动作文件
|
||
'.mocap': '🎬', # 动作捕捉数据
|
||
|
||
# 其他常见文件
|
||
'.log': '📋', # 日志文件
|
||
'.tmp': '🗂️', # 临时文件
|
||
'.bak': '💾', # 备份文件
|
||
'.old': '📦', # 旧文件
|
||
}
|
||
|
||
# 返回对应的图标,如果找不到则返回默认文档图标
|
||
return icon_map.get(ext, '📄')
|
||
|
||
def startDrag(self, supportedActions):
|
||
"""开始拖拽操作"""
|
||
selected_items = self.selectedItems()
|
||
if not selected_items:
|
||
return
|
||
|
||
# 创建 MIME 数据
|
||
mimeData = QMimeData()
|
||
|
||
# 收集文件路径用于向外拖拽
|
||
urls = []
|
||
internal_paths = []
|
||
|
||
for item in selected_items:
|
||
filepath = item.data(0, Qt.UserRole)
|
||
if filepath:
|
||
internal_paths.append(filepath)
|
||
# 检查是否是模型文件(用于向外拖拽)
|
||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
urls.append(QUrl.fromLocalFile(filepath))
|
||
|
||
# 设置内部拖拽数据
|
||
mimeData.setText('\n'.join(internal_paths))
|
||
|
||
# 设置向外拖拽数据
|
||
if urls:
|
||
mimeData.setUrls(urls)
|
||
|
||
# 创建拖拽对象
|
||
drag = QDrag(self)
|
||
drag.setMimeData(mimeData)
|
||
|
||
# 设置拖拽图标
|
||
pixmap = QPixmap(32, 32)
|
||
pixmap.fill(Qt.transparent)
|
||
painter = QPainter(pixmap)
|
||
painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(selected_items)))
|
||
painter.end()
|
||
drag.setPixmap(pixmap)
|
||
|
||
# 执行拖拽
|
||
drag.exec_(supportedActions)
|
||
|
||
def dragEnterEvent(self, event):
|
||
"""处理拖拽进入事件"""
|
||
# 检查是否是内部拖拽
|
||
if event.mimeData().hasText():
|
||
event.acceptProposedAction()
|
||
else:
|
||
event.ignore()
|
||
|
||
def dragMoveEvent(self, event):
|
||
"""处理拖拽移动事件"""
|
||
if not event.mimeData().hasText():
|
||
event.ignore()
|
||
return
|
||
|
||
# 获取目标项
|
||
target_item = self.itemAt(event.pos())
|
||
selected_items = self.selectedItems()
|
||
|
||
# 检查是否拖拽到多选区域内的项目
|
||
if target_item and target_item in selected_items:
|
||
event.ignore()
|
||
return
|
||
|
||
# 检查是否拖拽到自己的子级
|
||
if target_item:
|
||
for selected_item in selected_items:
|
||
if self._isChildOf(target_item, selected_item):
|
||
event.ignore()
|
||
return
|
||
|
||
# 如果拖到文件夹,允许拖到文件夹内部
|
||
if target_item:
|
||
is_folder = target_item.data(0, Qt.UserRole + 1)
|
||
if is_folder:
|
||
# 接受拖放,显示指示框
|
||
event.acceptProposedAction()
|
||
return
|
||
|
||
|
||
# 调用父类方法处理插入指示线
|
||
event.accept()
|
||
super().dragMoveEvent(event)
|
||
|
||
def _isChildOf(self, potential_child, potential_parent):
|
||
"""检查 potential_child 是否是 potential_parent 的子级"""
|
||
current = potential_child.parent()
|
||
while current:
|
||
if current == potential_parent:
|
||
return True
|
||
current = current.parent()
|
||
return False
|
||
|
||
def dropEvent(self, event):
|
||
"""处理拖放事件"""
|
||
if not event.mimeData().hasText():
|
||
event.ignore()
|
||
return
|
||
|
||
drag_paths = event.mimeData().text().split('\n')
|
||
if not drag_paths:
|
||
event.ignore()
|
||
return
|
||
|
||
# 获取目标项
|
||
target_item = self.itemAt(event.pos())
|
||
if not target_item:
|
||
# 如果拖到空白处,默认放到根目录
|
||
target_path = self.current_path
|
||
else:
|
||
# 如果是文件夹,就放到里面
|
||
is_folder = target_item.data(0, Qt.UserRole + 1)
|
||
if is_folder:
|
||
target_path = target_item.data(0, Qt.UserRole)
|
||
else:
|
||
# 如果是文件,则放到其父目录
|
||
parent_item = target_item.parent()
|
||
if parent_item:
|
||
target_path = parent_item.data(0, Qt.UserRole)
|
||
else:
|
||
target_path = self.current_path
|
||
|
||
# 执行移动
|
||
self._moveFiles(drag_paths, target_path)
|
||
event.acceptProposedAction()
|
||
# 让 Qt 更新界面
|
||
super().dropEvent(event)
|
||
|
||
def _moveFiles(self, source_paths, target_dir):
|
||
"""移动文件到目标目录"""
|
||
import os
|
||
import shutil
|
||
from PyQt5.QtWidgets import QMessageBox
|
||
|
||
moved_files = []
|
||
failed_files = []
|
||
|
||
for source_path in source_paths:
|
||
if not source_path or not os.path.exists(source_path):
|
||
continue
|
||
|
||
if os.path.isdir(source_path) and target_dir.startswith(source_path):
|
||
failed_files.append(f"{source_path} (不能移动到子目录)")
|
||
continue
|
||
|
||
if os.path.dirname(source_path) == target_dir:
|
||
continue
|
||
|
||
filename = os.path.basename(source_path)
|
||
target_path = os.path.join(target_dir, filename)
|
||
|
||
if os.path.exists(target_path):
|
||
failed_files.append(f"{filename} (目标已存在)")
|
||
continue
|
||
|
||
try:
|
||
shutil.move(source_path, target_path)
|
||
moved_files.append(filename)
|
||
print(f"移动文件: {source_path} -> {target_path}")
|
||
except Exception as e:
|
||
failed_files.append(f"{filename} ({str(e)})")
|
||
|
||
# 使用状态保持刷新
|
||
if moved_files:
|
||
self._refreshWithStatePreservation()
|
||
|
||
if failed_files:
|
||
QMessageBox.warning(
|
||
self,
|
||
"移动失败",
|
||
f"以下文件移动失败:\n" + "\n".join(failed_files)
|
||
)
|
||
|
||
if moved_files:
|
||
print(f"成功移动 {len(moved_files)} 个文件")
|
||
|
||
# def mouseDoubleClickEvent(self, event):
|
||
# """处理双击事件"""
|
||
# item = self.itemAt(event.pos())
|
||
# if item:
|
||
# filepath = item.data(0, Qt.UserRole)
|
||
# is_folder = item.data(0, Qt.UserRole + 1)
|
||
#
|
||
# if is_folder:
|
||
# # 文件夹:展开/折叠
|
||
# item.setExpanded(not item.isExpanded())
|
||
# else:
|
||
# # 文件:检查是否是模型文件
|
||
# if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
# self.world.importModel(filepath)
|
||
# else:
|
||
# # 其他文件:用系统默认程序打开
|
||
# self.openFile(filepath)
|
||
#
|
||
# super().mouseDoubleClickEvent(event)
|
||
|
||
|
||
class CustomConsoleDockWidget(QWidget):
|
||
"""自定义控制台停靠部件"""
|
||
|
||
def __init__(self, world, parent=None):
|
||
if parent is None:
|
||
parent = wrapinstance(0, QWidget)
|
||
super().__init__(parent)
|
||
self.world = world
|
||
self.setupUI()
|
||
self.setupConsoleRedirect()
|
||
|
||
def setupUI(self):
|
||
"""初始化控制台UI"""
|
||
layout = QVBoxLayout(self)
|
||
|
||
# 控制台工具栏
|
||
toolbar = QHBoxLayout()
|
||
|
||
# 清空按钮
|
||
self.clearBtn = QPushButton("清空")
|
||
self.clearBtn.clicked.connect(self.clearConsole)
|
||
toolbar.addWidget(self.clearBtn)
|
||
|
||
# 自动滚动开关
|
||
self.autoScrollBtn = QPushButton("自动滚动")
|
||
self.autoScrollBtn.setCheckable(True)
|
||
self.autoScrollBtn.setChecked(True)
|
||
toolbar.addWidget(self.autoScrollBtn)
|
||
|
||
# 时间戳开关
|
||
self.timestampBtn = QPushButton("显示时间")
|
||
self.timestampBtn.setCheckable(True)
|
||
self.timestampBtn.setChecked(True)
|
||
toolbar.addWidget(self.timestampBtn)
|
||
|
||
toolbar.addStretch()
|
||
layout.addLayout(toolbar)
|
||
|
||
# 控制台文本区域
|
||
from PyQt5.QtWidgets import QTextEdit
|
||
self.consoleText = QTextEdit()
|
||
self.consoleText.setReadOnly(True)
|
||
self.consoleText.setStyleSheet("""
|
||
QTextEdit {
|
||
background-color: #1e1e1e;
|
||
color: #ffffff;
|
||
font-family: 'Consolas', 'Monaco', monospace;
|
||
font-size: 10pt;
|
||
border: 1px solid #3e3e3e;
|
||
}
|
||
""")
|
||
layout.addWidget(self.consoleText)
|
||
|
||
# # 命令输入区域
|
||
# inputLayout = QHBoxLayout()
|
||
# inputLayout.addWidget(QLabel(">>> "))
|
||
#
|
||
# self.commandInput = QLineEdit()
|
||
# self.commandInput.setStyleSheet("""
|
||
# QLineEdit {
|
||
# background-color: #2d2d2d;
|
||
# color: #ffffff;
|
||
# font-family: 'Consolas', 'Monaco', monospace;
|
||
# font-size: 10pt;
|
||
# border: 1px solid #3e3e3e;
|
||
# padding: 5px;
|
||
# }
|
||
# """)
|
||
# self.commandInput.returnPressed.connect(self.executeCommand)
|
||
# inputLayout.addWidget(self.commandInput)
|
||
#
|
||
# layout.addLayout(inputLayout)
|
||
|
||
# 添加欢迎信息
|
||
self.addMessage("🎮 编辑器控制台已启动", "INFO")
|
||
|
||
def setupConsoleRedirect(self):
|
||
"""设置控制台重定向"""
|
||
import sys
|
||
|
||
# 保存原始的stdout和stderr
|
||
self.original_stdout = sys.stdout
|
||
self.original_stderr = sys.stderr
|
||
|
||
# 创建自定义输出流
|
||
sys.stdout = ConsoleRedirect(self, "STDOUT")
|
||
sys.stderr = ConsoleRedirect(self, "STDERR")
|
||
|
||
def addMessage(self, message, msg_type="INFO"):
|
||
"""添加消息到控制台"""
|
||
import datetime
|
||
|
||
# 获取当前时间
|
||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||
|
||
# 根据消息类型设置颜色
|
||
color_map = {
|
||
"INFO": "#ffffff", # 白色
|
||
"WARNING": "#ffaa00", # 橙色
|
||
"ERROR": "#ff4444", # 红色
|
||
"SUCCESS": "#44ff44", # 绿色
|
||
"STDOUT": "#cccccc", # 浅灰色
|
||
"STDERR": "#ff6666", # 浅红色
|
||
}
|
||
|
||
color = color_map.get(msg_type, "#ffffff")
|
||
|
||
# 构建HTML格式的消息
|
||
if self.timestampBtn.isChecked():
|
||
html_message = f'<span style="color: #888888;">[{timestamp}]</span> <span style="color: {color};">{message}</span>'
|
||
else:
|
||
html_message = f'<span style="color: {color};">{message}</span>'
|
||
|
||
# 添加到控制台
|
||
self.consoleText.append(html_message)
|
||
|
||
# 自动滚动到底部
|
||
if self.autoScrollBtn.isChecked():
|
||
scrollbar = self.consoleText.verticalScrollBar()
|
||
scrollbar.setValue(scrollbar.maximum())
|
||
|
||
def clearConsole(self):
|
||
"""清空控制台"""
|
||
self.consoleText.clear()
|
||
self.addMessage("控制台已清空", "INFO")
|
||
|
||
def executeCommand(self):
|
||
"""执行命令"""
|
||
command = self.commandInput.text().strip()
|
||
if not command:
|
||
return
|
||
|
||
# 显示输入的命令
|
||
self.addMessage(f">>> {command}", "INFO")
|
||
self.commandInput.clear()
|
||
|
||
try:
|
||
# 执行Python命令
|
||
if hasattr(self.world, 'executeCommand'):
|
||
result = self.world.executeCommand(command)
|
||
if result:
|
||
self.addMessage(str(result), "SUCCESS")
|
||
else:
|
||
# 简单的eval执行
|
||
result = eval(command)
|
||
if result is not None:
|
||
self.addMessage(str(result), "SUCCESS")
|
||
|
||
except Exception as e:
|
||
self.addMessage(f"错误: {str(e)}", "ERROR")
|
||
|
||
def cleanup(self):
|
||
"""清理资源"""
|
||
import sys
|
||
|
||
# 恢复原始的stdout和stderr
|
||
if hasattr(self, 'original_stdout'):
|
||
sys.stdout = self.original_stdout
|
||
if hasattr(self, 'original_stderr'):
|
||
sys.stderr = self.original_stderr
|
||
|
||
|
||
class ConsoleRedirect:
|
||
"""控制台重定向类"""
|
||
|
||
def __init__(self, console_widget, stream_type):
|
||
self.console_widget = console_widget
|
||
self.stream_type = stream_type
|
||
|
||
def write(self, text):
|
||
"""重定向写入"""
|
||
if text.strip(): # 忽略空行
|
||
self.console_widget.addMessage(text.strip(), self.stream_type)
|
||
|
||
def flush(self):
|
||
"""刷新缓冲区"""
|
||
pass
|
||
|
||
class CustomTreeWidget(QTreeWidget):
|
||
"""自定义场景树部件"""
|
||
|
||
def __init__(self, world, parent=None):
|
||
if parent is None:
|
||
parent = wrapinstance(0, QWidget)
|
||
super().__init__(parent)
|
||
self.world = world
|
||
# self.selectedItems = None
|
||
self.initData()
|
||
self.setupUI() # 初始化界面
|
||
self.setupContextMenu() # 初始化右键菜单
|
||
|
||
self.setupDragDrop() # 设置拖拽功能
|
||
|
||
self.original_scales={}
|
||
|
||
def initData(self):
|
||
"""初始化变量"""
|
||
# 定义2D GUI元素类型
|
||
self.gui_2d_types = {
|
||
"GUI_BUTTON", # DirectButton
|
||
"GUI_LABEL", # DirectLabel
|
||
"GUI_ENTRY", # DirectEntry
|
||
"GUI_IMAGE",
|
||
"GUI_NODE" # 其他2D GUI容器
|
||
}
|
||
|
||
# 定义3D GUI元素类型
|
||
self.gui_3d_types = {
|
||
"GUI_3DTEXT", # 3D TextNode
|
||
"GUI_3DIMAGE",
|
||
"GUI_VIRTUAL_SCREEN" # Virtual Screen
|
||
}
|
||
|
||
# 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素)
|
||
self.scene_3d_types = {
|
||
"SCENE_ROOT",
|
||
"SCENE_NODE",
|
||
"LIGHT_NODE",
|
||
"CAMERA_NODE",
|
||
"IMPORTED_MODEL_NODE",
|
||
"MODEL_NODE",
|
||
"TERRAIN_NODE",
|
||
"CESIUM_TILESET_NODE"
|
||
}
|
||
|
||
# 这是一个最佳实践,它让代码的意图变得非常清晰。
|
||
self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types)
|
||
|
||
def setupUI(self):
|
||
"""初始化UI设置"""
|
||
self.setHeaderHidden(True)
|
||
# 启用多选和拖拽
|
||
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||
self.setDropIndicatorShown(True) # 启用拖放指示线
|
||
|
||
def setupDragDrop(self):
|
||
"""设置拖拽功能"""
|
||
# 使用自定义拖拽模式
|
||
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop
|
||
self.setDefaultDropAction(Qt.DropAction.MoveAction)
|
||
self.setDragEnabled(True)
|
||
self.setAcceptDrops(True)
|
||
|
||
def setupContextMenu(self):
|
||
"""设置右键菜单"""
|
||
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||
self.customContextMenuRequested.connect(self.showContextMenu)
|
||
|
||
def showContextMenu(self, position):
|
||
"""显示右键菜单 - 复用主菜单的创建动作"""
|
||
if self.selectedItems():
|
||
item = self.selectedItems()[0]
|
||
print(f"为项目 '{item.text(0)}' 显示右键菜单")
|
||
|
||
# 创建右键菜单
|
||
menu = QMenu(self)
|
||
|
||
# 获取主窗口的创建菜单动作 - 关键修改
|
||
if hasattr(self.world, 'main_window'):
|
||
main_window = self.world.main_window
|
||
create_actions = main_window.getCreateMenuActions()
|
||
|
||
# 创建子菜单 - 复用主菜单结构
|
||
createMenu = menu.addMenu('创建')
|
||
|
||
# 基础对象
|
||
createMenu.addAction(create_actions['createEmpty'])
|
||
|
||
# 3D对象菜单
|
||
create3dObjectMenu = createMenu.addMenu('3D对象')
|
||
|
||
# 3D GUI菜单
|
||
create3dGUIMenu = createMenu.addMenu('3D GUI')
|
||
create3dGUIMenu.addAction(create_actions['create3DText'])
|
||
create3dGUIMenu.addAction(create_actions['create3DImage'])
|
||
|
||
# GUI菜单
|
||
createGUIMenu = createMenu.addMenu('GUI')
|
||
createGUIMenu.addAction(create_actions['createButton'])
|
||
createGUIMenu.addAction(create_actions['createLabel'])
|
||
createGUIMenu.addAction(create_actions['createEntry'])
|
||
createGUIMenu.addAction(create_actions['createImage'])
|
||
createGUIMenu.addSeparator()
|
||
createGUIMenu.addAction(create_actions['createVideoScreen'])
|
||
createGUIMenu.addAction(create_actions['createSphericalVideo'])
|
||
createGUIMenu.addSeparator()
|
||
createGUIMenu.addAction(create_actions['createVirtualScreen'])
|
||
|
||
# 光源菜单
|
||
createLightMenu = createMenu.addMenu('光源')
|
||
createLightMenu.addAction(create_actions['createSpotLight'])
|
||
createLightMenu.addAction(create_actions['createPointLight'])
|
||
|
||
else:
|
||
# 备用方案:如果无法获取主窗口动作,显示提示
|
||
createMenu = menu.addMenu('创建')
|
||
noActionItem = createMenu.addAction('功能不可用')
|
||
noActionItem.setEnabled(False)
|
||
|
||
# 添加删除选项
|
||
menu.addSeparator()
|
||
deleteAction = menu.addAction('删除')
|
||
if hasattr(self, 'delete_items'):
|
||
deleteAction.triggered.connect(lambda: self.delete_items(self.selectedItems()))
|
||
|
||
# 显示菜单
|
||
global_pos = self.mapToGlobal(position)
|
||
action = menu.exec_(global_pos)
|
||
|
||
print(f"右键菜单已显示在位置: {global_pos}")
|
||
if action:
|
||
print(f"用户选择了动作: {action.text()}")
|
||
else:
|
||
print("用户取消了菜单选择")
|
||
|
||
# 在 CustomTreeWidget 类的 dropEvent 方法中替换缩放处理部分
|
||
def dropEvent(self, event):
|
||
# 1. 获取所有被拖拽的项
|
||
dragged_items = self.selectedItems()
|
||
if not dragged_items:
|
||
event.ignore()
|
||
return
|
||
|
||
# 2. 在执行Qt的默认拖拽前,记录所有拖拽项的原始状态
|
||
drag_info = []
|
||
for item in dragged_items:
|
||
panda_node = item.data(0, Qt.UserRole)
|
||
if not panda_node or panda_node.is_empty():
|
||
continue # 跳过无效节点
|
||
|
||
drag_info.append({
|
||
"item": item,
|
||
"node": panda_node,
|
||
"old_parent_node": item.parent().data(0, Qt.UserRole) if item.parent() else None
|
||
})
|
||
|
||
# 3. 执行Qt的默认拖拽,让UI树先行更新
|
||
# 这一步会自动处理移动或复制,并将项目从旧父节点移除,添加到新父节点
|
||
super().dropEvent(event)
|
||
|
||
# 4. 遍历记录下的信息,同步每一个Panda3D节点的状态
|
||
try:
|
||
for info in drag_info:
|
||
dragged_item = info["item"]
|
||
dragged_node = info["node"]
|
||
old_parent_node = info["old_parent_node"]
|
||
|
||
# 获取拖拽后的新父节点
|
||
new_parent_item = dragged_item.parent()
|
||
new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None
|
||
|
||
# 仅当父节点实际发生变化时才执行重新父化
|
||
if old_parent_node != new_parent_node:
|
||
print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}")
|
||
|
||
# # 保存世界坐标位置
|
||
# 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:
|
||
if is_2d_gui:
|
||
# 2D GUI元素需要特殊处理
|
||
if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1":
|
||
# 目标是GUI元素,直接重新父化
|
||
dragged_node.wrtReparentTo(new_parent_node)
|
||
else:
|
||
# 目标是3D节点,保持GUI特性,重新父化到aspect2d
|
||
# from direct.showbase.ShowBase import aspect2d
|
||
dragged_node.wrtReparentTo(self.world.aspect2d)
|
||
print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下")
|
||
else:
|
||
# 非GUI元素正常重新父化
|
||
dragged_node.wrtReparentTo(new_parent_node)
|
||
else:
|
||
# 如果新父节点为None,根据元素类型决定父节点
|
||
if is_2d_gui:
|
||
# from direct.showbase.ShowBase import aspect2d
|
||
dragged_node.wrtReparentTo(self.world.aspect2d)
|
||
print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d")
|
||
else:
|
||
dragged_node.wrtReparentTo(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)
|
||
|
||
print(f"✅ Panda3D父子关系已更新")
|
||
else:
|
||
print(f"同层级移动:父节点未变化,跳过Panda3D重新父化")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 同步Panda3D场景图失败: {e}")
|
||
# 不影响Qt树的更新,继续执行
|
||
|
||
# 事后验证:确保节点仍在"场景"根节点下
|
||
self._ensureUnderSceneRoot(dragged_item)
|
||
self.world.property_panel._syncEffectiveVisibility(dragged_node)
|
||
event.accept()
|
||
|
||
def _ensureUnderSceneRoot(self, item):
|
||
"""确保节点在场景根节点下,如果不是则自动修正"""
|
||
if not item:
|
||
return
|
||
|
||
# 检查是否成为了顶级节点
|
||
if not item.parent():
|
||
# 通过数据标识判断是否是场景根节点
|
||
scene_root_marker = item.data(0, Qt.UserRole + 1)
|
||
if scene_root_marker != "SCENE_ROOT":
|
||
print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...")
|
||
|
||
# 找到场景根节点
|
||
scene_root = None
|
||
for i in range(self.topLevelItemCount()):
|
||
top_item = self.topLevelItem(i)
|
||
if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT":
|
||
scene_root = top_item
|
||
break
|
||
|
||
if scene_root:
|
||
# 将节点移回场景根节点下
|
||
self.takeTopLevelItem(self.indexOfTopLevelItem(item))
|
||
scene_root.addChild(item)
|
||
print(f"✅ 已将节点 {item.text(0)} 移回场景根节点下")
|
||
|
||
def isValidParentChild(self, dragged_item, target_item):
|
||
"""检查是否是有效的父子关系(防止循环)+ GUI类型验证"""
|
||
|
||
# 1. 禁止拖放到自身
|
||
if dragged_item == target_item:
|
||
return False
|
||
|
||
# 2. 禁止拖到根节点之外(根节点本身除外)
|
||
target_root = self._getRootNode(target_item)
|
||
if not target_root or target_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT":
|
||
print(f"❌ 目标节点 {target_item.text(0)} 不在场景下")
|
||
return False
|
||
|
||
# 3. 禁止拖拽场景根节点
|
||
dragged_root = self._getRootNode(dragged_item)
|
||
if (dragged_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT" or
|
||
not dragged_root or dragged_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT"):
|
||
print(f"❌ 禁止拖拽场景根节点或根节点外的节点")
|
||
return False
|
||
|
||
# 4. Qt 树循环检查
|
||
current = target_item
|
||
while current:
|
||
if current == dragged_item:
|
||
print(f"❌ Qt 树检测:{target_item.text(0)} 是 {dragged_item.text(0)} 的后代")
|
||
return False
|
||
current = current.parent()
|
||
|
||
# 5. GUI元素类型验证 - 新增功能
|
||
if not self._validateGUITypeCompatibility(dragged_item, target_item):
|
||
return False
|
||
|
||
return True
|
||
|
||
def _validateGUITypeCompatibility(self, dragged_item, target_item):
|
||
"""验证GUI元素类型兼容性"""
|
||
try:
|
||
# 获取节点类型标识
|
||
dragged_type = dragged_item.data(0, Qt.UserRole + 1)
|
||
target_type = target_item.data(0, Qt.UserRole + 1)
|
||
|
||
# 定义2D GUI元素类型
|
||
gui_2d_types = self.gui_2d_types
|
||
|
||
# 定义3D GUI元素类型
|
||
gui_3d_types = self.gui_3d_types
|
||
|
||
# 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素)
|
||
scene_3d_types = self.scene_3d_types
|
||
|
||
# 检查拖拽元素的类型
|
||
is_dragged_2d_gui = dragged_type in gui_2d_types
|
||
is_dragged_3d_gui = dragged_type in gui_3d_types
|
||
is_dragged_3d_scene = dragged_type in scene_3d_types
|
||
|
||
# 检查目标的类型
|
||
is_target_2d_gui = target_type in gui_2d_types
|
||
is_target_3d_gui = target_type in gui_3d_types
|
||
is_target_3d_scene = target_type in scene_3d_types
|
||
|
||
# === 严格的类型隔离验证逻辑 ===
|
||
|
||
# 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下
|
||
if is_dragged_2d_gui:
|
||
if target_type == "SCENE_ROOT":
|
||
return True
|
||
elif is_target_2d_gui:
|
||
print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}")
|
||
return True
|
||
elif is_target_3d_gui:
|
||
print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D GUI元素 {target_item.text(0)} 下")
|
||
print(" 💡 提示: 2D GUI元素只能作为其他2D GUI元素的子节点")
|
||
return False
|
||
elif is_target_3d_scene:
|
||
print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D场景节点 {target_item.text(0)} 下")
|
||
print(" 💡 提示: 2D GUI元素应该保持在2D GUI层级结构中")
|
||
return False
|
||
else:
|
||
print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
|
||
return False
|
||
|
||
# 2. 3D GUI元素的拖拽限制 - 只能拖拽到3D场景节点下
|
||
elif is_dragged_3d_gui:
|
||
if is_target_3d_scene:
|
||
print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
|
||
return True
|
||
elif is_target_2d_gui:
|
||
print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下")
|
||
print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系")
|
||
return False
|
||
elif is_target_3d_gui:
|
||
print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到其他3D GUI元素 {target_item.text(0)} 下")
|
||
print(" 💡 提示: 允许3D GUI元素之间建立父子关系")
|
||
return True
|
||
else:
|
||
print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
|
||
return False
|
||
|
||
# 3. 3D场景元素的拖拽限制 - 只能拖拽到3D场景节点或3D GUI元素下
|
||
elif is_dragged_3d_scene:
|
||
if is_target_3d_scene:
|
||
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}")
|
||
return True
|
||
elif is_target_2d_gui:
|
||
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下")
|
||
print(" 💡 提示: 3D场景元素不能与2D GUI元素建立父子关系")
|
||
print(" 💡 建议: 将3D场景元素拖拽到其他3D场景节点或场景根节点下")
|
||
return False
|
||
elif is_target_3d_gui:
|
||
print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下")
|
||
print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下")
|
||
return True
|
||
else:
|
||
print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
|
||
return False
|
||
|
||
# 4. 其他未分类元素 - 严格禁止拖拽到2D GUI下
|
||
else:
|
||
if is_target_2d_gui:
|
||
print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下")
|
||
print(" 💡 提示: 只有2D GUI元素才能作为其他2D GUI元素的子节点")
|
||
return False
|
||
elif is_target_3d_gui or is_target_3d_scene:
|
||
print(f"✅ 允许元素 {dragged_item.text(0)} 拖拽到3D节点 {target_item.text(0)}")
|
||
return True
|
||
else:
|
||
print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下")
|
||
return False
|
||
|
||
# 默认情况(理论上不应该到达这里)
|
||
print(f"⚠️ GUI类型验证遇到未处理的情况: {dragged_type} -> {target_type}")
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"❌ GUI类型兼容性验证失败: {str(e)}")
|
||
# 出错时采用保守策略,禁止拖拽
|
||
return False
|
||
|
||
def _getRootNode(self, item):
|
||
"""获取树中节点的根节点项"""
|
||
if not item:
|
||
return None
|
||
|
||
current = item
|
||
while current.parent():
|
||
current = current.parent()
|
||
return current
|
||
|
||
def dragEnterEvent(self, event):
|
||
"""处理拖入事件"""
|
||
if event.source() == self:
|
||
event.accept()
|
||
else:
|
||
event.ignore()
|
||
|
||
def dragMoveEvent(self, event):
|
||
"""处理拖动事件"""
|
||
indicator_pos = self.dropIndicatorPosition()
|
||
indicator_str = "Unknown"
|
||
|
||
if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem:
|
||
indicator_str = "OnItem"
|
||
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem:
|
||
indicator_str = "AboveItem"
|
||
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem:
|
||
indicator_str = "BelowItem"
|
||
elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport:
|
||
indicator_str = "OnViewport"
|
||
|
||
print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})')
|
||
|
||
if event.source() != self:
|
||
event.ignore()
|
||
return
|
||
|
||
# 获取当前拖拽的项目和目标位置
|
||
target_item = self.itemAt(event.pos())
|
||
selected_items = self.selectedItems()
|
||
|
||
# 检查是否拖拽到多选区域内的项目
|
||
if target_item and target_item in selected_items:
|
||
event.ignore()
|
||
return
|
||
|
||
# 检查其他禁止条件
|
||
if target_item and selected_items:
|
||
for dragged_item in selected_items:
|
||
if not self.isValidParentChild(dragged_item, target_item):
|
||
event.ignore()
|
||
return
|
||
|
||
super().dragMoveEvent(event)
|
||
event.accept()
|
||
|
||
def keyPressEvent(self, event):
|
||
"""处理键盘按键事件"""
|
||
if event.key() == Qt.Key_Delete:
|
||
# currentItem = self.currentItem()
|
||
# if currentItem and currentItem.parent():
|
||
# # 检查是否是模型节点或其子节点
|
||
# if self.world.interface_manager.isModelOrChild(currentItem):
|
||
# nodePath = currentItem.data(0, Qt.UserRole)
|
||
# if nodePath:
|
||
# print("正在删除节点...")
|
||
# self.world.interface_manager.deleteNode(nodePath, currentItem)
|
||
# print("删除完成")
|
||
selected_items = self.selectedItems()
|
||
if selected_items:
|
||
# 执行删除操作
|
||
# if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE":
|
||
# self._preprocess_light_items_for_deletion(selected_items)
|
||
self.delete_items(selected_items)
|
||
else:
|
||
# 没有选中任何项目,执行默认操作
|
||
super().keyPressEvent(event)
|
||
else:
|
||
super().keyPressEvent(event)
|
||
|
||
def _preprocess_light_items_for_deletion(self, selected_items):
|
||
"""预处理灯光节点删除,特别处理最后一个灯光节点的问题"""
|
||
if not selected_items:
|
||
return selected_items
|
||
|
||
# 检查选中的项目中是否包含灯光节点
|
||
light_items = []
|
||
for item in selected_items:
|
||
node_type = item.data(0, Qt.UserRole + 1)
|
||
if node_type == "LIGHT_NODE":
|
||
light_items.append(item)
|
||
|
||
# 如果没有灯光节点,直接返回
|
||
if not light_items:
|
||
return selected_items
|
||
|
||
# 检查是否只有最后一个灯光节点被选中
|
||
processed_items = list(selected_items) # 创建副本
|
||
|
||
for item in light_items:
|
||
panda_node = item.data(0, Qt.UserRole)
|
||
if not panda_node:
|
||
continue
|
||
|
||
# 获取灯光类型
|
||
if hasattr(panda_node, 'getTag'):
|
||
light_type = panda_node.getTag("light_type")
|
||
|
||
# 检查是否是最后一个Spotlight
|
||
if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and
|
||
self.world.Spotlight and self.world.Spotlight[-1] == panda_node and
|
||
len(self.world.Spotlight) > 1):
|
||
|
||
print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}")
|
||
# 这里可以添加特殊处理逻辑,比如提示用户或阻止删除
|
||
|
||
# 检查是否是最后一个Pointlight
|
||
elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and
|
||
self.world.Pointlight and self.world.Pointlight[-1] == panda_node and
|
||
len(self.world.Pointlight) > 1):
|
||
|
||
print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}")
|
||
# 这里可以添加特殊处理逻辑,比如提示用户或阻止删除
|
||
|
||
return processed_items
|
||
|
||
def delete_items(self, selected_items):
|
||
"""删除选中的item - 简化版本"""
|
||
if not selected_items:
|
||
return
|
||
|
||
# 1. 过滤掉不能删除的节点
|
||
deletable_items = []
|
||
for item in selected_items:
|
||
node_type = item.data(0, Qt.UserRole + 1)
|
||
panda_node = item.data(0, Qt.UserRole)
|
||
|
||
# 跳过场景根节点和主相机
|
||
if (node_type == "SCENE_ROOT" or
|
||
(panda_node and hasattr(self.world, 'cam') and panda_node == self.world.cam)):
|
||
continue
|
||
deletable_items.append(item)
|
||
|
||
if not deletable_items:
|
||
QMessageBox.information(self, "提示", "没有可删除的节点")
|
||
return
|
||
|
||
# 2. 确认删除
|
||
item_count = len(deletable_items)
|
||
if item_count == 1:
|
||
message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?"
|
||
else:
|
||
message = f"确定要删除 {item_count} 个节点吗?"
|
||
|
||
reply = QMessageBox.question(
|
||
self, "确认删除", message,
|
||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
|
||
)
|
||
|
||
if reply != QMessageBox.Yes:
|
||
return
|
||
|
||
# 默认选中场景根节点,通常是第一个顶级节点
|
||
next_item_to_select = self.topLevelItem(0)
|
||
|
||
# 3. 执行删除循环
|
||
deleted_count = 0
|
||
for item in deletable_items:
|
||
try:
|
||
# 在删除前,记录其父节点,作为删除后的新选择
|
||
# 选择最后一个被删除项的父节点作为新的焦点
|
||
if item.parent():
|
||
next_item_to_select = item.parent()
|
||
panda_node = item.data(0, Qt.UserRole)
|
||
|
||
if panda_node:
|
||
# 清理选择状态
|
||
if (hasattr(self.world, 'selection') and
|
||
hasattr(self.world.selection, 'selectedNode') and
|
||
self.world.selection.selectedNode == panda_node):
|
||
self.world.selection.updateSelection(None)
|
||
|
||
# 清理特殊资源(参考interface_manager.py的逻辑)
|
||
if hasattr(self.world, 'property_panel'):
|
||
self.world.property_panel.removeActorForModel(panda_node)
|
||
|
||
# 清理灯光
|
||
if hasattr(panda_node, 'getPythonTag'):
|
||
light_object = panda_node.getPythonTag('rp_light_object')
|
||
if light_object and hasattr(self.world, 'render_pipeline'):
|
||
print(f'11111111111111111111111111,{light_object.casts_shadows}')
|
||
self.world.render_pipeline.remove_light(light_object)
|
||
|
||
# 从world列表中移除
|
||
if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements:
|
||
self.world.gui_elements.remove(panda_node)
|
||
if hasattr(self.world, 'models') and panda_node in self.world.models:
|
||
self.world.models.remove(panda_node)
|
||
if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight:
|
||
self.world.Spotlight.remove(panda_node)
|
||
if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight:
|
||
self.world.Pointlight.remove(panda_node)
|
||
if hasattr(self.world, 'terrains') and panda_node in self.world.terrains:
|
||
self.world.terrains.remove(panda_node)
|
||
if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets:
|
||
# self.world.tilesets.remove(panda_node)
|
||
# 从 tilesets 列表中移除
|
||
if hasattr(self.world, 'scene_manager'):
|
||
tilesets_to_remove = []
|
||
for i, tileset_info in enumerate(self.world.scene_manager.tilesets):
|
||
if tileset_info['node'] == panda_node:
|
||
tilesets_to_remove.append(i)
|
||
|
||
# 从后往前删除,避免索引问题
|
||
for i in reversed(tilesets_to_remove):
|
||
del self.world.scene_manager.tilesets[i]
|
||
|
||
# 从Panda3D场景中移除
|
||
try:
|
||
if not panda_node.isEmpty():
|
||
panda_node.removeNode()
|
||
except Exception as e:
|
||
print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}")
|
||
|
||
# 从Qt树中移除
|
||
parent_item = item.parent()
|
||
if parent_item:
|
||
parent_item.removeChild(item)
|
||
else:
|
||
index = self.indexOfTopLevelItem(item)
|
||
if index >= 0:
|
||
self.takeTopLevelItem(index)
|
||
|
||
deleted_count += 1
|
||
print(f"✅ 删除节点: {item.text(0)}")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
# 最终清理
|
||
# if hasattr(self.world, 'property_panel'):
|
||
# self.world.property_panel.clearPropertyPanel()
|
||
|
||
# 4. 删除操作完成后,更新UI ---
|
||
if deleted_count > 0:
|
||
print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...")
|
||
|
||
# 检查预备选择的节点是否还有效 (例如,父节点可能也一同被删了)
|
||
# 如果next_item_to_select在树中找不到了,就退回到选择根节点
|
||
if next_item_to_select and self.indexFromItem(next_item_to_select).isValid():
|
||
new_selection_item = next_item_to_select
|
||
else:
|
||
# 如果之前的父节点也一并被删除了,就默认选择场景根节点
|
||
new_selection_item = self.topLevelItem(0)
|
||
|
||
if new_selection_item:
|
||
# 设置UI树的选择
|
||
self.setCurrentItem(new_selection_item)
|
||
# 获取对应的Panda3D节点
|
||
new_panda_node = new_selection_item.data(0, Qt.UserRole)
|
||
# 调用您提供的函数来更新选择状态和属性面板
|
||
self.update_selection_and_properties(new_panda_node, new_selection_item)
|
||
else:
|
||
# 如果连根节点都没有了(例如清空场景),则清空选择
|
||
self.update_selection_and_properties(None, None)
|
||
|
||
def delete_item(self, panda_node):
|
||
"""删除指定节点 panda3D(node)- 优化和修复版本"""
|
||
if not panda_node or panda_node.is_empty():
|
||
print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。")
|
||
return
|
||
|
||
# --- 关键修复:在操作前,安全地获取节点名字 ---
|
||
node_name_for_logging = panda_node.getName()
|
||
|
||
# 1. 寻找对应的Qt Item
|
||
item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot())
|
||
|
||
# 场景清理(无论是否找到item,都应该执行)
|
||
self._cleanup_panda_node_resources(panda_node)
|
||
panda_node.removeNode()
|
||
|
||
# 如果没有找到item,说明UI已经移除或不同步,清理完Panda3D资源后即可退出
|
||
if not item:
|
||
print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。")
|
||
return
|
||
|
||
try:
|
||
# 2. 过滤受保护节点
|
||
node_type = item.data(0, Qt.UserRole + 1)
|
||
if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中
|
||
print(f"ℹ️ 节点 {item.text(0)} 是受保护节点,无法删除。")
|
||
return
|
||
|
||
# 3. 从UI树中移除
|
||
parent_for_next_selection = item.parent()
|
||
if item.parent():
|
||
item.parent().removeChild(item)
|
||
else:
|
||
index = self.indexOfTopLevelItem(item)
|
||
if index >= 0:
|
||
self.takeTopLevelItem(index)
|
||
|
||
print(f"✅ 成功删除节点: {node_name_for_logging}")
|
||
|
||
# 4. 更新UI
|
||
print(f"🔄 正在更新UI...")
|
||
if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid():
|
||
new_selection_item = parent_for_next_selection
|
||
else:
|
||
new_selection_item = self.topLevelItem(0)
|
||
|
||
if new_selection_item:
|
||
self.setCurrentItem(new_selection_item)
|
||
new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole)
|
||
self.update_selection_and_properties(new_panda_node_to_select, new_selection_item)
|
||
else:
|
||
self.update_selection_and_properties(None, None)
|
||
|
||
except Exception as e:
|
||
print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def _cleanup_panda_node_resources(self, panda_node):
|
||
"""一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。"""
|
||
if not panda_node or panda_node.is_empty():
|
||
return
|
||
try:
|
||
# 清理选择状态
|
||
if hasattr(self.world, 'selection') and self.world.selection.selectedNode == panda_node:
|
||
self.world.selection.updateSelection(None)
|
||
# 清理属性面板
|
||
if hasattr(self.world, 'property_panel'):
|
||
self.world.property_panel.removeActorForModel(panda_node)
|
||
# 清理灯光
|
||
if hasattr(panda_node, 'getPythonTag'):
|
||
light_object = panda_node.getPythonTag('rp_light_object')
|
||
if light_object and hasattr(self.world, 'render_pipeline'):
|
||
self.world.render_pipeline.remove_light(light_object)
|
||
# 从各种world管理列表中移除
|
||
lists_to_check = ['gui_elements', 'models', 'Spotlight', 'Pointlight', 'terrains']
|
||
for list_name in lists_to_check:
|
||
if hasattr(self.world, list_name):
|
||
world_list = getattr(self.world, list_name)
|
||
if panda_node in world_list:
|
||
world_list.remove(panda_node)
|
||
# 特殊处理tilesets
|
||
if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'tilesets'):
|
||
tilesets_to_remove = [i for i, info in enumerate(self.world.scene_manager.tilesets) if
|
||
info.get('node') == panda_node]
|
||
for i in reversed(tilesets_to_remove):
|
||
del self.world.scene_manager.tilesets[i]
|
||
print(f"🧹 已清理节点 {panda_node.getName()} 的所有关联资源。")
|
||
except Exception as e:
|
||
# 即便这里出错,也要打印信息,但不要让整个删除流程中断
|
||
print(f"⚠️ 清理节点 {panda_node.getName()} 资源时出错: {e}")
|
||
|
||
# def mousePressEvent(self, event):
|
||
# """鼠标按下事件"""
|
||
# if event.button() == Qt.LeftButton:
|
||
# if self.currentItem():
|
||
# print(f"self.currentItem() = {self.currentItem()}")
|
||
# else:
|
||
# print(f"self.currentItem() = None")
|
||
#
|
||
# # 调用父类处理其他事件
|
||
# super().mousePressEvent(event)
|
||
|
||
def update_item_name(self, text, item):
|
||
""" 树节点名字 """
|
||
if not item:
|
||
return
|
||
try:
|
||
# 正确的代码
|
||
node = item.data(0, Qt.UserRole)
|
||
|
||
item.setText(0, text)
|
||
node.setName(text)
|
||
except Exception as e:
|
||
print(e)
|
||
|
||
def _findSceneRoot(self):
|
||
"""查找场景根节点"""
|
||
for i in range(self.topLevelItemCount()):
|
||
top_item = self.topLevelItem(i)
|
||
if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT":
|
||
return top_item
|
||
return None
|
||
|
||
def create_model_items(self, model):
|
||
if not model:
|
||
print("传入的参数model为空")
|
||
return
|
||
# 创建根节点项
|
||
# root_item = QTreeWidgetItem(self)
|
||
# root_item.setText(0, model.getName() or "Unnamed Node")
|
||
# root_item.setText(1, model.node().getTypeName())
|
||
# root_item.setIcon(0, self.item_icons.get('model', self.item_icons['default']))
|
||
|
||
# 存储NodePath引用以便后续操作
|
||
# root_item.setData(0, Qt.UserRole, model)
|
||
root_item = self._findSceneRoot()
|
||
|
||
# 递归添加子节点
|
||
self._add_children_recursive(root_item, model)
|
||
|
||
return root_item
|
||
|
||
def _add_children_recursive(self, parent_item, node_path: NodePath):
|
||
"""递归添加子节点到树项"""
|
||
print(f'开始递归添加子节点')
|
||
# 获取所有子节点
|
||
children = node_path.getChildren()
|
||
|
||
for i in range(children.getNumPaths()):
|
||
child_node: NodePath = children.getPath(i)
|
||
|
||
# 过滤条件
|
||
if not child_node.hasTag("is_scene_element"):
|
||
print(f"不存在------------------------{child_node.hasTag('is_scene_element')}")
|
||
continue
|
||
|
||
print(f"存在------------------------{child_node.getName()}")
|
||
# 创建子项
|
||
child_item = QTreeWidgetItem(parent_item)
|
||
child_item.setText(0, child_node.getName() or f"Child_{i}")
|
||
|
||
# 存储NodePath引用
|
||
child_item.setData(0, Qt.UserRole, child_node)
|
||
|
||
# 添加额外信息(可选)
|
||
# self._add_node_info(child_item, child_node)
|
||
|
||
# 递归处理子节点的子节点
|
||
if child_node.getNumChildren() > 0:
|
||
self._add_children_recursive(child_item, child_node)
|
||
|
||
# ==================== 辅助方法 ====================
|
||
def _findSceneRoot(self):
|
||
"""查找场景根节点"""
|
||
for i in range(self.topLevelItemCount()):
|
||
top_item = self.topLevelItem(i)
|
||
if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT":
|
||
return top_item
|
||
return
|
||
|
||
def _generateUniqueNodeName(self, base_name, parent_node):
|
||
"""生成唯一的节点名称"""
|
||
# 获取父节点下所有子节点的名称
|
||
existing_names = set()
|
||
for child in parent_node.getChildren():
|
||
existing_names.add(child.getName())
|
||
|
||
# 如果基础名称不存在,直接使用
|
||
if base_name not in existing_names:
|
||
return base_name
|
||
|
||
# 否则添加数字后缀
|
||
counter = 1
|
||
while f"{base_name}_{counter}" in existing_names:
|
||
counter += 1
|
||
|
||
return f"{base_name}_{counter}"
|
||
|
||
def add_node_to_tree_widget(self, node, parent_item, node_type):
|
||
"""将node元素添加到树形控件"""
|
||
|
||
# BLACK_LIST 和依赖项导入保持不变
|
||
BLACK_LIST = {'', '**', 'temp', 'collision'}
|
||
from panda3d.core import CollisionNode, ModelRoot
|
||
from PyQt5.QtWidgets import QTreeWidgetItem
|
||
from PyQt5.QtCore import Qt
|
||
|
||
# 1. 修改内部函数,让它返回创建的节点
|
||
def addNodeToTree(node, parentItem, force=False):
|
||
"""内部递归函数,现在会返回创建的顶级节点项"""
|
||
if not force and should_skip(node):
|
||
return None # 如果跳过,返回None
|
||
|
||
nodeItem = QTreeWidgetItem(parentItem, [node.getName()])
|
||
nodeItem.setData(0, Qt.UserRole, node)
|
||
nodeItem.setData(0, Qt.UserRole + 1, node_type)
|
||
|
||
for child in node.getChildren():
|
||
# 递归调用,但我们只关心顶级的nodeItem
|
||
addNodeToTree(child, nodeItem, force=False)
|
||
|
||
return nodeItem # <-- 新增:返回创建的QTreeWidgetItem
|
||
|
||
def should_skip(node):
|
||
name = node.getName()
|
||
return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance(
|
||
node.node(), ModelRoot) or name == ""
|
||
|
||
# 使用一个变量来确保无论哪个分支都有返回值
|
||
new_qt_item = None
|
||
node_name = ""
|
||
|
||
try:
|
||
if node_type == "IMPORTED_MODEL_NODE":
|
||
# getTag('file') 可能是你自己设置的tag,这里假设它存在
|
||
node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName()
|
||
|
||
# 2. 接收 addNodeToTree 的返回值
|
||
new_qt_item = addNodeToTree(node, parent_item, force=True)
|
||
|
||
else:
|
||
node_name = node.getName() if hasattr(node, 'getName') else "node"
|
||
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
|
||
new_qt_item.setData(0, Qt.UserRole, node)
|
||
new_qt_item.setData(0, Qt.UserRole + 1, node_type)
|
||
|
||
# 确保 new_qt_item 成功创建后再继续操作
|
||
if new_qt_item:
|
||
# 展开父节点
|
||
if hasattr(parent_item, 'setExpanded'):
|
||
parent_item.setExpanded(True)
|
||
|
||
print(f"✅ Qt树节点添加成功: {node_name}")
|
||
return new_qt_item
|
||
else:
|
||
# 如果 addNodeToTree 因为 should_skip 返回了 None
|
||
print(f"ℹ️ 节点 {node_name} 被跳过,未添加到树中。")
|
||
return None
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
print(f"❌ 添加node到树形控件失败: {str(e)}")
|
||
traceback.print_exc() # 打印更详细的错误堆栈,方便调试
|
||
return None
|
||
|
||
def update_selection_and_properties(self, node, qt_item):
|
||
"""更新选择状态和属性面板"""
|
||
try:
|
||
# 更新选择状态
|
||
if hasattr(self.world, 'selection'):
|
||
self.world.selection.updateSelection(node)
|
||
|
||
# 更新属性面板
|
||
if hasattr(self.world, 'property_panel'):
|
||
self.world.property_panel.updatePropertyPanel(qt_item)
|
||
elif hasattr(self.world, 'updatePropertyPanel'):
|
||
self.world.updatePropertyPanel(qt_item)
|
||
|
||
print(f"✅ 更新选择和属性面板: {qt_item.text(0)}")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 更新选择和属性面板失败: {str(e)}")
|
||
|
||
# ==================== 3D辅助方法 ====================
|
||
def get_target_parents_for_creation(self):
|
||
"""获取创建目标的父节点列表"""
|
||
from PyQt5.QtCore import Qt
|
||
|
||
target_parents = []
|
||
|
||
try:
|
||
selected_items = self.selectedItems()
|
||
|
||
# 检查选中的项目,找出所有有效的父节点
|
||
for item in selected_items:
|
||
if self._isValidParentForNewNode(item):
|
||
parent_node = item.data(0, Qt.UserRole)
|
||
if parent_node:
|
||
target_parents.append((item, parent_node))
|
||
print(f"📍 找到有效父节点: {item.text(0)}")
|
||
|
||
# 如果没有有效的选中节点,使用场景根节点
|
||
if not target_parents:
|
||
print("⚠️ 没有有效选中节点,使用场景根节点")
|
||
scene_root = self._findSceneRoot()
|
||
if scene_root:
|
||
parent_node = scene_root.data(0, Qt.UserRole)
|
||
if parent_node:
|
||
target_parents.append((scene_root, parent_node))
|
||
else:
|
||
# 如果没有_findSceneRoot方法,使用world.render作为默认父节点
|
||
if hasattr(self.world, 'render'):
|
||
# 创建一个虚拟的树项目来表示render节点
|
||
class MockTreeItem:
|
||
def text(self, column):
|
||
return "render"
|
||
|
||
mock_item = MockTreeItem()
|
||
target_parents.append((mock_item, self.world.render))
|
||
|
||
print(f"📊 总共找到 {len(target_parents)} 个目标父节点")
|
||
return target_parents
|
||
|
||
except Exception as e:
|
||
print(f"❌ 获取目标父节点失败: {str(e)}")
|
||
return []
|
||
|
||
def _isValidParentForNewNode(self, item):
|
||
"""检查节点是否适合作为新节点的父节点-3d"""
|
||
if not item:
|
||
return False
|
||
|
||
# 获取节点类型标识
|
||
node_type = item.data(0, Qt.UserRole + 1)
|
||
|
||
# 场景根节点和普通场景节点可以作为父节点
|
||
if node_type in self.valid_3d_parent_types:
|
||
return True
|
||
|
||
# # 模型节点也可以作为父节点
|
||
# panda_node = item.data(0, Qt.UserRole)
|
||
# if panda_node and panda_node.hasTag("is_model_root"):
|
||
# return True
|
||
#
|
||
# # 其他类型的节点也可以,但排除一些特殊情况
|
||
# if panda_node:
|
||
# # # 排除相机节点
|
||
# # if panda_node == self.world.cam:
|
||
# # return False
|
||
# # 排除碰撞节点
|
||
# from panda3d.core import CollisionNode
|
||
# if isinstance(panda_node.node(), CollisionNode):
|
||
# return False
|
||
# return True
|
||
|
||
return False
|
||
|
||
|
||
def get_target_parents_for_gui_creation(self):
|
||
"""获取GUI创建目标的父节点列表 - 支持GUI元素作为父节点"""
|
||
from PyQt5.QtCore import Qt
|
||
|
||
target_parents = []
|
||
|
||
try:
|
||
selected_items = self.selectedItems()
|
||
|
||
# 检查选中的项目,找出所有有效的父节点
|
||
for item in selected_items:
|
||
if self.isValidParentForGUI(item):
|
||
parent_node = item.data(0, Qt.UserRole)
|
||
if parent_node:
|
||
target_parents.append((item, parent_node)) # 修复:确保添加到列表
|
||
print(f"📍 找到有效GUI父节点: {item.text(0)}")
|
||
else:
|
||
print(f"⚠️ GUI父节点 {item.text(0)} 的Panda3D数据为空")
|
||
|
||
# 如果没有有效的选中节点,使用场景根节点
|
||
if not target_parents:
|
||
print("⚠️ 没有有效选中节点,使用场景根节点")
|
||
scene_root = self._findSceneRoot()
|
||
if scene_root:
|
||
parent_node = scene_root.data(0, Qt.UserRole)
|
||
if parent_node:
|
||
target_parents.append((scene_root, parent_node))
|
||
else:
|
||
if hasattr(self.world, 'render'):
|
||
class MockTreeItem:
|
||
def text(self, column):
|
||
return "render"
|
||
|
||
mock_item = MockTreeItem()
|
||
target_parents.append((mock_item, self.world.render))
|
||
|
||
print(f"📊 总共找到 {len(target_parents)} 个目标父节点")
|
||
return target_parents
|
||
|
||
except Exception as e:
|
||
print(f"❌ 获取目标父节点失败: {str(e)}")
|
||
return []
|
||
|
||
def isValidParentForGUI(self, item):
|
||
"""检查节点是否适合作为GUI元素的父节点"""
|
||
if not item:
|
||
return False
|
||
|
||
# 获取节点类型标识
|
||
node_type = item.data(0, Qt.UserRole + 1)
|
||
|
||
# GUI元素可以作为其他GUI元素的父节点
|
||
if node_type in self.gui_2d_types:
|
||
return True
|
||
|
||
# 场景根节点和普通场景节点也可以作为父节点
|
||
if node_type in ["SCENE_ROOT"]:
|
||
return True
|
||
|
||
return False
|
||
|
||
def is_gui_element(self, node):
|
||
"""判断节点是否是GUI元素"""
|
||
if not node:
|
||
return False
|
||
|
||
# 检查是否有GUI标签
|
||
if hasattr(node, 'getTag'):
|
||
return node.getTag("is_gui_element") == "1" or node.getTag("gui_type") in ["info_panel", "button", "label", "entry", "3d_text", "virtual_screen"]
|
||
|
||
# 检查是否是DirectGUI对象
|
||
try:
|
||
from direct.gui.DirectGuiBase import DirectGuiBase
|
||
return isinstance(node, DirectGuiBase)
|
||
except ImportError:
|
||
return False
|
||
|
||
def calculate_relative_gui_position(self, pos, parent_gui):
|
||
"""计算相对于GUI父节点的位置"""
|
||
try:
|
||
# 对于GUI子元素,使用相对坐标
|
||
# 这里使用较小的缩放因子,因为是相对于父GUI的位置
|
||
relative_pos = (pos[0] * 0.05, 0, pos[2] * 0.05)
|
||
print(f"📐 计算GUI相对位置: {pos} -> {relative_pos}")
|
||
return relative_pos
|
||
except Exception as e:
|
||
print(f"❌ 计算GUI相对位置失败: {str(e)}")
|
||
# 如果计算失败,返回默认的屏幕坐标
|
||
return (pos[0] * 0.1, 0, pos[2] * 0.1)
|
||
|
||
#---------------------------暂时无用-------------------------------
|
||
|
||
def add_existing_node(self, panda_node, node_type="SCENE_NODE", parent_item=None):
|
||
"""将已存在的Panda3D节点添加到Qt树形控件中
|
||
|
||
Args:
|
||
panda_node: 已创建的Panda3D节点
|
||
node_type: 节点类型标识
|
||
parent_item: 父Qt项目,如果为None则使用当前选中项或根节点
|
||
|
||
Returns:
|
||
QTreeWidgetItem: 创建的Qt树项目
|
||
"""
|
||
try:
|
||
if not panda_node:
|
||
print("❌ 传入的Panda3D节点为空")
|
||
return None
|
||
|
||
# 确定父项目 - 保持简单,只处理单个父节点
|
||
if parent_item is None:
|
||
# 优先使用当前选中的第一个有效节点作为父节点
|
||
selected_items = self.selectedItems()
|
||
if selected_items:
|
||
# 找到第一个有效的父节点
|
||
for potential_parent in selected_items:
|
||
if self._isValidParentForNewNode(potential_parent):
|
||
parent_item = potential_parent
|
||
print(f"📍 使用选中节点作为父节点: {parent_item.text(0)}")
|
||
break
|
||
|
||
# 如果没有找到有效的选中节点
|
||
if not parent_item:
|
||
print("⚠️ 所有选中节点都不适合作为父节点,查找场景根节点")
|
||
parent_item = self._findSceneRoot()
|
||
else:
|
||
# 没有选中任何节点,使用场景根节点
|
||
print("📍 没有选中节点,使用场景根节点作为父节点")
|
||
parent_item = self._findSceneRoot()
|
||
|
||
# 如果场景根节点也找不到,最后尝试render节点
|
||
if not parent_item:
|
||
print("📍 场景根节点未找到,尝试使用render节点")
|
||
parent_item = self._findRenderItem()
|
||
|
||
if not parent_item:
|
||
print("❌ 无法找到合适的父节点")
|
||
return None
|
||
|
||
# 创建Qt树项目
|
||
node_name = panda_node.getName()
|
||
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
|
||
new_qt_item.setData(0, Qt.UserRole, panda_node)
|
||
new_qt_item.setData(0, Qt.UserRole + 1, node_type)
|
||
|
||
# 展开父节点
|
||
parent_item.setExpanded(True)
|
||
|
||
print(f"✅ 成功将现有节点添加到树形控件: {node_name} -> 父节点: {parent_item.text(0)}")
|
||
return new_qt_item
|
||
|
||
except Exception as e:
|
||
print(f"❌ 添加现有节点到树形控件失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
def _findRenderItem(self):
|
||
"""查找render根节点项目"""
|
||
try:
|
||
root = self.invisibleRootItem()
|
||
for i in range(root.childCount()):
|
||
item = root.child(i)
|
||
if item.text(0) == "render":
|
||
return item
|
||
|
||
# 如果没找到render节点,返回第一个子项目
|
||
if root.childCount() > 0:
|
||
return root.child(0)
|
||
|
||
return None
|
||
except Exception as e:
|
||
print(f"查找render节点失败: {e}")
|
||
return None
|
||
|
||
def create_item(self, node_type="empty", selected_items=None):
|
||
"""创建不同类型的场景节点
|
||
|
||
Args:
|
||
node_type: 节点类型 ("empty", "spot_light", "point_light")
|
||
selected_items: 选中的父节点项目列表,如果为None则使用当前选中项或根节点
|
||
"""
|
||
try:
|
||
# 确定父节点
|
||
parent_items = self._determineParentItems(selected_items)
|
||
if not parent_items:
|
||
print("⚠️ 无法确定父节点")
|
||
return []
|
||
|
||
created_nodes = []
|
||
|
||
for parent_item in parent_items:
|
||
# 验证父节点的有效性
|
||
if not self._isValidParentForNewNode(parent_item):
|
||
print(f"⚠️ 节点 {parent_item.text(0)} 不适合作为父节点")
|
||
continue
|
||
|
||
# 获取父节点的Panda3D对象
|
||
parent_node = parent_item.data(0, Qt.UserRole)
|
||
if not parent_node:
|
||
print(f"⚠️ 父节点 {parent_item.text(0)} 没有对应的Panda3D对象")
|
||
continue
|
||
|
||
# 根据节点类型创建不同的节点
|
||
if node_type == "empty":
|
||
new_node = self._createEmptyNode(parent_node, parent_item)
|
||
elif node_type == "spot_light":
|
||
new_node = self._createSpotLightNode(parent_node, parent_item)
|
||
elif node_type == "point_light":
|
||
new_node = self._createPointLightNode(parent_node, parent_item)
|
||
else:
|
||
print(f"❌ 不支持的节点类型: {node_type}")
|
||
continue
|
||
|
||
if new_node:
|
||
created_nodes.append(new_node)
|
||
|
||
# 如果只创建了一个节点,自动选中它
|
||
if len(created_nodes) == 1:
|
||
_, qt_item = created_nodes[0]
|
||
self.setCurrentItem(qt_item)
|
||
# 更新选择和属性面板
|
||
if hasattr(self.world, 'selection'):
|
||
self.world.selection.updateSelection(qt_item.data(0, Qt.UserRole))
|
||
if hasattr(self.world, 'property_panel'):
|
||
self.world.property_panel.updatePropertyPanel(qt_item)
|
||
|
||
print(f"✅ 总共创建了 {len(created_nodes)} 个 {node_type} 节点")
|
||
return created_nodes
|
||
|
||
except Exception as e:
|
||
print(f"❌ 创建 {node_type} 节点失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return []
|
||
|
||
def _determineParentItems(self, selected_items):
|
||
"""确定父节点项目列表"""
|
||
if selected_items is not None:
|
||
return selected_items
|
||
|
||
# 使用当前选中的项目
|
||
current_selected = self.selectedItems()
|
||
if current_selected:
|
||
return current_selected
|
||
|
||
# 如果没有选中任何项目,使用场景根节点
|
||
scene_root = self._findSceneRoot()
|
||
if scene_root:
|
||
return [scene_root]
|
||
|
||
return []
|
||
|
||
def _setupNewNodeDefaults(self, node):
|
||
"""设置新节点的默认属性"""
|
||
# 设置默认位置(相对于父节点)
|
||
node.setPos(0, 0, 0)
|
||
node.setHpr(0, 0, 0)
|
||
node.setScale(1, 1, 1)
|
||
|
||
# 设置默认可见性
|
||
node.show()
|
||
|
||
# 可以根据需要添加更多默认设置
|
||
# 例如:默认材质、碰撞检测等
|
||
|
||
# ==================== 创建节点 ====================
|
||
def _createEmptyNode(self, parent_node, parent_item):
|
||
"""创建空节点"""
|
||
# 生成唯一的节点名称
|
||
node_name = self._generateUniqueNodeName("空节点", parent_node)
|
||
|
||
# 在Panda3D场景中创建新节点
|
||
new_panda_node = parent_node.attachNewNode(node_name)
|
||
|
||
# 设置新节点的默认属性
|
||
self._setupNewNodeDefaults(new_panda_node)
|
||
|
||
# 设置节点标签
|
||
new_panda_node.setTag("is_scene_element", "1")
|
||
new_panda_node.setTag("node_type", "empty_node")
|
||
new_panda_node.setTag("created_by_user", "1")
|
||
|
||
# 在Qt树中创建对应的项目
|
||
new_qt_item = QTreeWidgetItem(parent_item, [node_name])
|
||
new_qt_item.setData(0, Qt.UserRole, new_panda_node)
|
||
new_qt_item.setData(0, Qt.UserRole + 1, "SCENE_NODE")
|
||
|
||
# 展开父节点
|
||
parent_item.setExpanded(True)
|
||
|
||
print(f"✅ 成功创建空节点: {node_name}")
|
||
return (new_panda_node, new_qt_item)
|
||
|
||
def _createSpotLightNode(self, parent_node, parent_item):
|
||
"""创建聚光灯节点"""
|
||
from RenderPipelineFile.rpcore import SpotLight
|
||
from QPanda3D.Panda3DWorld import get_render_pipeline
|
||
from panda3d.core import Vec3, NodePath
|
||
|
||
try:
|
||
render_pipeline = get_render_pipeline()
|
||
|
||
# 生成唯一的节点名称
|
||
light_name = self._generateUniqueNodeName(f"Spotlight_{len(self.world.Spotlight)}", parent_node)
|
||
|
||
# 创建挂载节点
|
||
light_np = NodePath(light_name)
|
||
light_np.reparentTo(parent_node)
|
||
|
||
# 创建聚光灯对象
|
||
light = SpotLight()
|
||
light.direction = Vec3(0, 0, -1)
|
||
light.fov = 70
|
||
light.set_color_from_temperature(5 * 1000.0)
|
||
light.energy = 5000
|
||
light.radius = 1000
|
||
light.casts_shadows = True
|
||
light.shadow_map_resolution = 256
|
||
light.setPos(0, 0, 0) # 相对于父节点的位置
|
||
|
||
# 添加到渲染管线
|
||
render_pipeline.add_light(light)
|
||
|
||
# 设置节点属性和标签
|
||
light_np.setTag("light_type", "spot_light")
|
||
light_np.setTag("is_scene_element", "1")
|
||
light_np.setTag("light_energy", str(light.energy))
|
||
light_np.setTag("created_by_user", "1")
|
||
|
||
# 保存光源对象引用
|
||
light_np.setPythonTag("rp_light_object", light)
|
||
|
||
# 添加到管理列表
|
||
self.world.Spotlight.append(light_np)
|
||
|
||
# 在Qt树中创建对应的项目
|
||
new_qt_item = QTreeWidgetItem(parent_item, [light_name])
|
||
new_qt_item.setData(0, Qt.UserRole, light_np)
|
||
new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE")
|
||
|
||
# 展开父节点
|
||
parent_item.setExpanded(True)
|
||
|
||
print(f"✅ 成功创建聚光灯: {light_name}")
|
||
|
||
except Exception as e:
|
||
print(f"❌ 创建聚光灯失败: {str(e)}")
|
||
return None
|
||
|
||
def _createPointLightNode(self, parent_node, parent_item):
|
||
"""创建点光源节点"""
|
||
from RenderPipelineFile.rpcore import PointLight
|
||
from QPanda3D.Panda3DWorld import get_render_pipeline
|
||
from panda3d.core import Vec3, NodePath
|
||
|
||
try:
|
||
render_pipeline = get_render_pipeline()
|
||
|
||
# 生成唯一的节点名称
|
||
light_name = self._generateUniqueNodeName(f"Pointlight_{len(self.world.Pointlight)}", parent_node)
|
||
|
||
# 创建挂载节点
|
||
light_np = NodePath(light_name)
|
||
light_np.reparentTo(parent_node)
|
||
|
||
# 创建点光源对象
|
||
light = PointLight()
|
||
light.setPos(0, 0, 0) # 相对于父节点的位置
|
||
light.energy = 5000
|
||
light.radius = 1000
|
||
light.inner_radius = 0.4
|
||
light.set_color_from_temperature(5 * 1000.0)
|
||
light.casts_shadows = True
|
||
light.shadow_map_resolution = 256
|
||
|
||
# 添加到渲染管线
|
||
render_pipeline.add_light(light)
|
||
|
||
# 设置节点属性和标签
|
||
light_np.setTag("light_type", "point_light")
|
||
light_np.setTag("is_scene_element", "1")
|
||
light_np.setTag("light_energy", str(light.energy))
|
||
light_np.setTag("created_by_user", "1")
|
||
|
||
# 保存光源对象引用
|
||
light_np.setPythonTag("rp_light_object", light)
|
||
|
||
# 添加到管理列表
|
||
self.world.Pointlight.append(light_np)
|
||
|
||
# 在Qt树中创建对应的项目
|
||
new_qt_item = QTreeWidgetItem(parent_item, [light_name])
|
||
new_qt_item.setData(0, Qt.UserRole, light_np)
|
||
new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE")
|
||
|
||
# 展开父节点
|
||
parent_item.setExpanded(True)
|
||
|
||
print(f"✅ 成功创建点光源: {light_name}")
|
||
return (light_np, new_qt_item)
|
||
|
||
except Exception as e:
|
||
print(f"❌ 创建点光源失败: {str(e)}")
|
||
return None
|
||
|
||
|
||
|
||
|
||
|