清理qt
This commit is contained in:
parent
036b68ef41
commit
5a9e31a195
@ -496,7 +496,7 @@ class MoveGizmo(DirectObject):
|
||||
def undo_last_move(self):
|
||||
"""
|
||||
Undo the last committed move.
|
||||
Default hotkey (from Qt -> Panda3D translation): Ctrl+Z => 'control-z'.
|
||||
Default hotkey: Ctrl+Z => 'control-z'.
|
||||
"""
|
||||
if not self._move_undo_stack:
|
||||
self._log("undo_last_move: stack empty")
|
||||
@ -530,7 +530,7 @@ class MoveGizmo(DirectObject):
|
||||
# --- Mouse helpers -------------------------------------------------
|
||||
def _get_normalized_mouse(self, extra) -> Optional[Point3]:
|
||||
"""
|
||||
Convert Qt pixel coordinates (from QPanda3DWidget) to Panda's
|
||||
Convert external UI pixel coordinates to Panda's
|
||||
normalized device coordinates (-1..1), or fall back to
|
||||
mouseWatcherNode when available.
|
||||
"""
|
||||
@ -538,14 +538,14 @@ class MoveGizmo(DirectObject):
|
||||
mouse = self.world.mouseWatcherNode.getMouse()
|
||||
return Point3(mouse.x, mouse.y, 0)
|
||||
|
||||
# 1) Extra payload from QPanda3DWidget (pixels)
|
||||
# 1) Extra payload from external UI (pixels)
|
||||
if isinstance(extra, dict) and "x" in extra and "y" in extra:
|
||||
parent = getattr(self.world, "parent", None)
|
||||
if parent is not None:
|
||||
w = max(parent.width(), 1)
|
||||
h = max(parent.height(), 1)
|
||||
nx = (extra["x"] / w) * 2.0 - 1.0
|
||||
# Qt origin is top‑left, Panda origin is center with +Y up
|
||||
# Pixel origin is top-left; Panda origin is center with +Y up.
|
||||
ny = 1.0 - (extra["y"] / h) * 2.0
|
||||
return Point3(nx, ny, 0)
|
||||
|
||||
|
||||
@ -54,10 +54,10 @@ from .events import GizmoEvent
|
||||
- 控件会根据摄像机距离自动缩放,屏幕大小保持近似不变
|
||||
|
||||
集成方式(示例):
|
||||
from QPanda3D.Panda3DWorld import Panda3DWorld
|
||||
from QPanda3DExamples.rotate_gizmo import RotateGizmo
|
||||
from direct.showbase.ShowBase import ShowBase
|
||||
from TransformGizmo.rotate_gizmo import RotateGizmo
|
||||
|
||||
world = Panda3DWorld()
|
||||
world = ShowBase()
|
||||
model_np = world.render.attachNewNode("Box")
|
||||
# ... 在 model_np 下加载模型
|
||||
|
||||
@ -72,7 +72,7 @@ from .events import GizmoEvent
|
||||
|
||||
鼠标事件要求:
|
||||
- 本类默认监听 Panda3D 的 "mouse1" / "mouse1-up" / "mouse-move" 事件
|
||||
- 如果从 Qt / 自定义 UI 传递鼠标像素坐标,可以在发送事件时传入 extra 字典:
|
||||
- 如果从外部 UI 传递鼠标像素坐标,可以在发送事件时传入 extra 字典:
|
||||
messenger.send("mouse1", [{"x": mouse_x, "y": mouse_y}])
|
||||
本类会自动将像素坐标转换到 [-1, 1] 的标准化设备坐标
|
||||
"""
|
||||
@ -955,7 +955,7 @@ class RotateGizmo(DirectObject):
|
||||
"""
|
||||
将鼠标转换到 Panda3D 的标准化设备坐标 [-1, 1]。
|
||||
|
||||
优先使用 Qt / 外部 UI 传入的像素坐标(extra 字典),
|
||||
优先使用外部 UI 传入的像素坐标(extra 字典),
|
||||
如果没有,则回退到 Panda3D 的 mouseWatcherNode。
|
||||
"""
|
||||
if self.world.mouseWatcherNode.hasMouse():
|
||||
@ -969,7 +969,7 @@ class RotateGizmo(DirectObject):
|
||||
w = max(parent.width(), 1)
|
||||
h = max(parent.height(), 1)
|
||||
nx = (extra["x"] / w) * 2.0 - 1.0
|
||||
# Qt 原点在左上,Panda3D 原点在中心,Y 轴向上
|
||||
# 像素坐标原点在左上,Panda3D 原点在中心,Y 轴向上
|
||||
ny = 1.0 - (extra["y"] / h) * 2.0
|
||||
return Point3(nx, ny, 0.0)
|
||||
|
||||
|
||||
@ -48,10 +48,10 @@ from .events import GizmoEvent
|
||||
- 控件会根据摄像机距离自动缩放,屏幕大小保持近似不变
|
||||
|
||||
集成方式(示例):
|
||||
from QPanda3D.Panda3DWorld import Panda3DWorld
|
||||
from QPanda3DExamples.scale_gizmo import ScaleGizmo
|
||||
from direct.showbase.ShowBase import ShowBase
|
||||
from TransformGizmo.scale_gizmo import ScaleGizmo
|
||||
|
||||
world = Panda3DWorld()
|
||||
world = ShowBase()
|
||||
model_np = world.render.attachNewNode("Box")
|
||||
# ... 在 model_np 下加载模型
|
||||
|
||||
@ -66,7 +66,7 @@ from .events import GizmoEvent
|
||||
|
||||
鼠标事件要求:
|
||||
- 本类默认监听 Panda3D 的 "mouse1" / "mouse1-up" / "mouse-move" 事件
|
||||
- 如果从 Qt / 自定义 UI 传递鼠标像素坐标,可以在发送事件时传入 extra 字典:
|
||||
- 如果从外部 UI 传递鼠标像素坐标,可以在发送事件时传入 extra 字典:
|
||||
messenger.send("mouse1", [{"x": mouse_x, "y": mouse_y}])
|
||||
本类会自动将像素坐标转换到 [-1, 1] 的标准化设备坐标
|
||||
"""
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
# 修改后的 InfoPanelManager.py
|
||||
from xml.sax.handler import property_encoding
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from direct.gui.DirectGui import DirectFrame, DirectLabel
|
||||
from direct.showbase.ShowBaseGlobal import aspect2d
|
||||
from panda3d.core import TextNode, Vec4, NodePath
|
||||
from direct.showbase.DirectObject import DirectObject
|
||||
import threading
|
||||
# 修改后的 InfoPanelManager.py
|
||||
from direct.gui.DirectGui import DirectFrame, DirectLabel
|
||||
from direct.showbase.ShowBaseGlobal import aspect2d
|
||||
from panda3d.core import TextNode, Vec4, NodePath
|
||||
from direct.showbase.DirectObject import DirectObject
|
||||
import threading
|
||||
import json
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
|
||||
from scene.tree_roles import TREE_USER_ROLE
|
||||
|
||||
|
||||
class InfoPanelManager(DirectObject):
|
||||
"""信息面板管理器,用于创建和管理2D信息面板"""
|
||||
@ -215,9 +213,15 @@ class InfoPanelManager(DirectObject):
|
||||
root_item = None
|
||||
for i in range(tree_widget.topLevelItemCount()):
|
||||
item = tree_widget.topLevelItem(i)
|
||||
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
|
||||
root_item = item
|
||||
break
|
||||
item_node = None
|
||||
if hasattr(item, "data"):
|
||||
try:
|
||||
item_node = item.data(0, TREE_USER_ROLE)
|
||||
except Exception:
|
||||
item_node = None
|
||||
if item.text(0) == "render" or item_node == self.world.render:
|
||||
root_item = item
|
||||
break
|
||||
|
||||
if root_item:
|
||||
# 使用现有的 add_node_to_tree_widget 方法添加节点
|
||||
@ -1306,11 +1310,12 @@ class InfoPanelManager(DirectObject):
|
||||
|
||||
print("✓ 示例天气信息面板已创建")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ 创建示例天气信息面板失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}")
|
||||
except Exception as e:
|
||||
print(f"✗ 创建示例天气信息面板失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if hasattr(self.world, "add_error_message"):
|
||||
self.world.add_error_message(f"创建示例天气信息面板时出错: {str(e)}")
|
||||
|
||||
def getRealWeatherData(self):
|
||||
"""获取真实天气数据"""
|
||||
|
||||
@ -11,7 +11,7 @@ from direct.showbase.ShowBaseGlobal import globalClock
|
||||
from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState,
|
||||
DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue,
|
||||
CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib,
|
||||
TransparencyAttrib, Vec4, CollisionCapsule)
|
||||
TransparencyAttrib, Vec4, CollisionCapsule, WindowProperties)
|
||||
from direct.task.TaskManagerGlobal import taskMgr
|
||||
import math
|
||||
from core.selection_outline import SelectionOutlineManager
|
||||
@ -93,38 +93,16 @@ class SelectionSystem:
|
||||
# ==================== 光标设置 ====================
|
||||
def _setCursor(self,cursor_type):
|
||||
try:
|
||||
from PyQt5.QtCore import Qt
|
||||
if self._current_cursor == cursor_type:
|
||||
return
|
||||
if hasattr(self.world,'main_window') and self.world.main_window:
|
||||
main_window = self.world.main_window
|
||||
else:
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
main_window = QApplication.activeWindow()
|
||||
if not main_window:
|
||||
windows = QApplication.topLevelWindows()
|
||||
for window in windows:
|
||||
if hasattr(window,'isVisible') and window.isVisible():
|
||||
main_window = window
|
||||
break
|
||||
if main_window:
|
||||
if cursor_type == "crosshair":
|
||||
main_window.setCursor(Qt.CrossCursor)
|
||||
elif cursor_type == "size_hor":
|
||||
main_window.setCursor(Qt.SizeHorCursor)
|
||||
elif cursor_type == "size_ver":
|
||||
main_window.setCursor(Qt.SizeVerCursor)
|
||||
elif cursor_type == "size_all":
|
||||
main_window.setCursor(Qt.SizeAllCursor)
|
||||
elif cursor_type == "pointing_hand":
|
||||
main_window.setCursor(Qt.PointingHandCursor)
|
||||
else:
|
||||
main_window.unsetCursor()
|
||||
self._current_cursor = cursor_type
|
||||
#print(f"光标已设置:{cursor_type}")
|
||||
self._current_cursor = cursor_type
|
||||
else:
|
||||
print("警告:无法获取主窗口,光标设置失败")
|
||||
if not hasattr(self.world, "win") or not self.world.win:
|
||||
return
|
||||
|
||||
# Panda3D 原生接口不支持直接切换系统光标形状,这里保留状态并确保光标可见。
|
||||
props = WindowProperties()
|
||||
props.setCursorHidden(False)
|
||||
self.world.win.requestProperties(props)
|
||||
self._current_cursor = cursor_type
|
||||
except Exception as e:
|
||||
print(f"设置光标失败{e}")
|
||||
def _resetCursor(self):
|
||||
|
||||
@ -91,10 +91,10 @@ class VRTestMode:
|
||||
self.vr_manager._disable_main_cam()
|
||||
|
||||
# 设置高帧率用于测试
|
||||
if hasattr(self.vr_manager.world, 'qtWidget') and self.vr_manager.world.qtWidget:
|
||||
if hasattr(self.vr_manager.world.qtWidget, 'synchronizer'):
|
||||
self.vr_manager.world.qtWidget.synchronizer.setInterval(int(1000/144))
|
||||
print("✓ 测试模式:Qt Timer设置为144Hz")
|
||||
host_widget = getattr(self.vr_manager.world, "host_widget", None)
|
||||
if host_widget and hasattr(host_widget, "synchronizer"):
|
||||
host_widget.synchronizer.setInterval(int(1000 / 144))
|
||||
print("✓ 测试模式:渲染同步器设置为144Hz")
|
||||
|
||||
# 初始化测试显示系统
|
||||
if not self._initialize_test_display():
|
||||
|
||||
@ -1893,12 +1893,11 @@ class VRManager(DirectObject):
|
||||
print(" 注意:Submit后立即调用WaitGetPoses是错误实现")
|
||||
self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟
|
||||
|
||||
# 🚀 动态调整Qt Timer频率以支持VR
|
||||
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
|
||||
if hasattr(self.world.qtWidget, 'synchronizer'):
|
||||
# 设置为144Hz,让OpenVR控制实际渲染节奏
|
||||
self.world.qtWidget.synchronizer.setInterval(int(1000/144))
|
||||
print("✓ Qt Timer调整为144Hz,让OpenVR控制VR渲染节奏")
|
||||
# 🚀 动态调整宿主同步器频率以支持VR
|
||||
host_widget = getattr(self.world, "host_widget", None)
|
||||
if host_widget and hasattr(host_widget, "synchronizer"):
|
||||
host_widget.synchronizer.setInterval(int(1000 / 144))
|
||||
print("✓ 渲染同步器调整为144Hz,让OpenVR控制VR渲染节奏")
|
||||
|
||||
# 🔧 关键修复:检测并重建缺失的手柄visualizer
|
||||
# 当渲染模式切换时,visualizer可能被清理但控制器对象仍存在
|
||||
@ -1952,11 +1951,11 @@ class VRManager(DirectObject):
|
||||
# 恢复主相机
|
||||
self._enable_main_cam()
|
||||
|
||||
# 恢复Qt Timer到60FPS
|
||||
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
|
||||
if hasattr(self.world.qtWidget, 'synchronizer'):
|
||||
self.world.qtWidget.synchronizer.setInterval(int(1000/60))
|
||||
print("✓ Qt Timer恢复为60Hz")
|
||||
# 恢复渲染同步器到60FPS
|
||||
host_widget = getattr(self.world, "host_widget", None)
|
||||
if host_widget and hasattr(host_widget, "synchronizer"):
|
||||
host_widget.synchronizer.setInterval(int(1000 / 60))
|
||||
print("✓ 渲染同步器恢复为60Hz")
|
||||
|
||||
print("✅ VR模式已禁用,手柄模型已隐藏")
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ class CoreWorld(ShowBase):
|
||||
global _global_render_pipeline
|
||||
|
||||
# 初始化基础属性
|
||||
self.qtWidget = None # Qt部件引用(用于获取准确的渲染区域尺寸)
|
||||
self.host_widget = None # 外部宿主窗口引用(用于获取准确渲染区域尺寸)
|
||||
|
||||
# 设置基本配置
|
||||
loadPrcFileData("", "show-frame-rate-meter 0")
|
||||
@ -686,16 +686,17 @@ class CoreWorld(ShowBase):
|
||||
print(f"警告: 加载中文字体时发生错误: {e}")
|
||||
self.chinese_font = None
|
||||
|
||||
def setQtWidget(self, widget):
|
||||
"""设置Qt部件引用"""
|
||||
self.qtWidget = widget
|
||||
print(f"✓ 设置Qt部件引用: {widget}")
|
||||
def setHostWidget(self, widget):
|
||||
"""设置外部宿主窗口引用。"""
|
||||
self.host_widget = widget
|
||||
print(f"✓ 设置宿主窗口引用: {widget}")
|
||||
|
||||
def getWindowSize(self):
|
||||
"""获取准确的窗口尺寸"""
|
||||
if self.qtWidget:
|
||||
# 优先使用Qt部件的实际尺寸
|
||||
width, height = self.qtWidget.getActualSize()
|
||||
widget = self.host_widget
|
||||
if widget and hasattr(widget, "getActualSize"):
|
||||
# 优先使用宿主窗口的实际尺寸
|
||||
width, height = widget.getActualSize()
|
||||
if width > 0 and height > 0:
|
||||
return width, height
|
||||
|
||||
@ -754,30 +755,28 @@ class CoreWorld(ShowBase):
|
||||
self.lastMouseX = evt['x']
|
||||
self.lastMouseY = evt['y']
|
||||
#
|
||||
# # 通过 Qt 窗口隐藏光标并捕获鼠标
|
||||
# # 通过宿主窗口隐藏光标并捕获鼠标
|
||||
# try:
|
||||
# if hasattr(self, 'qtWidget') and self.qtWidget:
|
||||
# from PyQt5.QtCore import Qt
|
||||
# self.qtWidget.setCursor(Qt.BlankCursor)
|
||||
# if hasattr(self, 'host_widget') and self.host_widget:
|
||||
# self.host_widget.setCursorHidden(True)
|
||||
# # 捕获鼠标,使其无法离开窗口
|
||||
# self.qtWidget.grabMouse()
|
||||
# self.host_widget.grabMouse()
|
||||
# except Exception as e:
|
||||
# print(f"通过 Qt 隐藏光标时出错: {e}")
|
||||
# print(f"隐藏光标时出错: {e}")
|
||||
|
||||
def mouseReleaseEventRight(self, evt):
|
||||
"""处理鼠标右键释放事件"""
|
||||
#print("右键释放")
|
||||
self.mouseRightPressed = False
|
||||
|
||||
# # 恢复 Qt 窗口光标并释放鼠标捕获
|
||||
# # 恢复宿主窗口光标并释放鼠标捕获
|
||||
# try:
|
||||
# if hasattr(self, 'qtWidget') and self.qtWidget:
|
||||
# from PyQt5.QtCore import Qt
|
||||
# self.qtWidget.unsetCursor() # 恢复默认光标
|
||||
# if hasattr(self, 'host_widget') and self.host_widget:
|
||||
# self.host_widget.setCursorHidden(False) # 恢复默认光标
|
||||
# # 释放鼠标捕获
|
||||
# self.qtWidget.releaseMouse()
|
||||
# self.host_widget.releaseMouse()
|
||||
# except Exception as e:
|
||||
# print(f"恢复 Qt 光标时出错: {e}")
|
||||
# print(f"恢复光标时出错: {e}")
|
||||
|
||||
def mouseMoveEvent(self, evt):
|
||||
"""处理鼠标移动事件 - 只处理相机旋转"""
|
||||
@ -966,7 +965,7 @@ class CoreWorld(ShowBase):
|
||||
return None
|
||||
|
||||
# 创建材质编辑器组件
|
||||
# 使用纯 Panda3D 实现,不依赖 PyQt5
|
||||
# 使用纯 Panda3D 实现,不依赖外部 GUI 框架
|
||||
print("材质编辑器组件已创建(使用 Panda3D 原生实现)")
|
||||
|
||||
material_widget.setLayout(layout)
|
||||
|
||||
@ -64,13 +64,13 @@ python main.py
|
||||
- 选择虚拟环境中的Python路径
|
||||
3. **验证环境**:检查状态栏显示正确的Python版本
|
||||
|
||||
## 📦 项目依赖说明
|
||||
|
||||
- **Panda3D**: 3D图形引擎
|
||||
- **PyQt5/PySide6**: GUI框架
|
||||
- **Pillow**: 图像处理
|
||||
- **python-dotenv**: 环境变量管理
|
||||
- **pyassimp**: 3D模型加载
|
||||
## 📦 项目依赖说明
|
||||
|
||||
- **Panda3D**: 3D图形引擎
|
||||
- **imgui-bundle / p3dimgui**: ImGui GUI框架
|
||||
- **Pillow**: 图像处理
|
||||
- **python-dotenv**: 环境变量管理
|
||||
- **pyassimp**: 3D模型加载
|
||||
|
||||
## 🌍 跨平台注意事项
|
||||
|
||||
@ -83,8 +83,8 @@ python main.py
|
||||
**Q: 无法安装Panda3D?**
|
||||
A: 确保系统有OpenGL支持,或使用conda安装
|
||||
|
||||
**Q: PyQt5无法运行?**
|
||||
A: 可能需要安装系统级GUI依赖
|
||||
**Q: ImGui界面显示异常?**
|
||||
A: 检查显卡驱动/OpenGL支持,并确认已安装 `imgui-bundle`
|
||||
|
||||
**Q: 导入错误?**
|
||||
A: 检查Python版本(需要3.10+)
|
||||
A: 检查Python版本(需要3.10+)
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
Panda3D>=1.10.15
|
||||
PyQt5>=5.15.9
|
||||
PySide6>=6.8.1
|
||||
Pillow>=9.0.1
|
||||
python-dotenv>=1.0.1
|
||||
pyassimp>=5.2.5
|
||||
Panda3D>=1.10.15
|
||||
imgui-bundle
|
||||
Pillow>=9.0.1
|
||||
python-dotenv>=1.0.1
|
||||
pyassimp>=5.2.5
|
||||
|
||||
@ -5,11 +5,10 @@ channels:
|
||||
dependencies:
|
||||
- python=3.10
|
||||
- pip
|
||||
- pip:
|
||||
- Panda3D>=1.10.15
|
||||
- PyQt5>=5.15.9
|
||||
- PySide6>=6.8.1
|
||||
- Pillow>=9.0.1
|
||||
- python-dotenv>=1.0.1
|
||||
- pyassimp>=5.2.5
|
||||
- pip:
|
||||
- Panda3D>=1.10.15
|
||||
- imgui-bundle
|
||||
- Pillow>=9.0.1
|
||||
- python-dotenv>=1.0.1
|
||||
- pyassimp>=5.2.5
|
||||
- six
|
||||
|
||||
@ -58,33 +58,21 @@ PyGObject==3.42.1
|
||||
PyJWT==2.3.0
|
||||
pymacaroons==0.13.0
|
||||
PyNaCl==1.5.0
|
||||
pyparsing==2.4.7
|
||||
PyQt5==5.15.9
|
||||
pyqt5-plugins==5.15.9.2.3
|
||||
PyQt5-Qt5==5.15.2
|
||||
pyqt5-tools==5.15.9.3.3
|
||||
PyQt5_sip==12.16.1
|
||||
pyRFC3339==1.1
|
||||
PySide6==6.8.1
|
||||
PySide6_Addons==6.8.1
|
||||
PySide6_Essentials==6.8.1
|
||||
python-apt==2.4.0+ubuntu4
|
||||
pyparsing==2.4.7
|
||||
pyRFC3339==1.1
|
||||
python-apt==2.4.0+ubuntu4
|
||||
python-dateutil==2.8.1
|
||||
python-debian==0.1.43+ubuntu1.1
|
||||
python-dotenv==1.0.1
|
||||
pytz==2022.1
|
||||
pyxdg==0.27
|
||||
PyYAML==5.4.1
|
||||
QPanda3D==0.2.10
|
||||
qt5-applications==5.15.2.2.3
|
||||
qt5-tools==5.15.2.1.3
|
||||
reportlab==3.6.8
|
||||
PyYAML==5.4.1
|
||||
reportlab==3.6.8
|
||||
requests==2.25.1
|
||||
SceneEditor==22.5
|
||||
screen-resolution-extra==0.0.0
|
||||
SecretStorage==3.3.1
|
||||
shiboken6==6.8.1
|
||||
six==1.16.0
|
||||
SecretStorage==3.3.1
|
||||
six==1.16.0
|
||||
soupsieve==2.3.1
|
||||
systemd-python==234
|
||||
ubuntu-drivers-common==0.0.0
|
||||
|
||||
@ -39,28 +39,45 @@ class SceneManagerConvertTilesMixin:
|
||||
|
||||
def _convertToGLBWithProgress(self, filepath):
|
||||
"""带进度显示的GLB转换"""
|
||||
try:
|
||||
from PyQt5.QtWidgets import QProgressDialog, QApplication
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
# 创建进度对话框
|
||||
progress = QProgressDialog("正在转换模型格式以获得更好的动画支持...", "取消", 0, 100)
|
||||
progress.setWindowTitle("模型格式转换")
|
||||
progress.setWindowModality(Qt.WindowModal)
|
||||
progress.show()
|
||||
QApplication.processEvents()
|
||||
|
||||
try:
|
||||
result = self._convertToGLB(filepath, progress)
|
||||
progress.hide()
|
||||
return result
|
||||
except Exception as e:
|
||||
progress.hide()
|
||||
print(f"转换过程出错: {e}")
|
||||
class _ConsoleProgress:
|
||||
def __init__(self, label):
|
||||
self._value = 0
|
||||
self._label = label
|
||||
|
||||
def setWindowTitle(self, title):
|
||||
print(f"[GLB转换] {title}")
|
||||
|
||||
def setWindowModality(self, _):
|
||||
return None
|
||||
|
||||
except ImportError:
|
||||
# 如果没有 PyQt5,直接转换
|
||||
|
||||
def show(self):
|
||||
print(f"[GLB转换] {self._label}")
|
||||
|
||||
def hide(self):
|
||||
print("[GLB转换] 完成")
|
||||
|
||||
def setValue(self, value):
|
||||
value = int(value)
|
||||
if value != self._value:
|
||||
self._value = value
|
||||
print(f"[GLB转换] 进度: {self._value}%")
|
||||
|
||||
def setLabelText(self, text):
|
||||
if text != self._label:
|
||||
self._label = text
|
||||
print(f"[GLB转换] {self._label}")
|
||||
|
||||
progress = _ConsoleProgress("正在转换模型格式以获得更好的动画支持...")
|
||||
progress.setWindowTitle("模型格式转换")
|
||||
progress.show()
|
||||
|
||||
try:
|
||||
result = self._convertToGLB(filepath, progress)
|
||||
progress.hide()
|
||||
return result
|
||||
except Exception as e:
|
||||
progress.hide()
|
||||
print(f"转换过程出错: {e}")
|
||||
return self._convertToGLB(filepath)
|
||||
|
||||
def _convertToGLB(self, filepath, progress=None):
|
||||
@ -71,8 +88,6 @@ class SceneManagerConvertTilesMixin:
|
||||
if progress:
|
||||
progress.setValue(10)
|
||||
progress.setLabelText("准备转换...")
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
QApplication.processEvents()
|
||||
|
||||
# 准备输出路径
|
||||
base_name = os.path.splitext(os.path.basename(filepath))[0]
|
||||
@ -93,7 +108,6 @@ class SceneManagerConvertTilesMixin:
|
||||
if progress:
|
||||
progress.setValue(20)
|
||||
progress.setLabelText("尝试 Blender 转换...")
|
||||
QApplication.processEvents()
|
||||
|
||||
# 方法1: 使用 Blender 进行转换
|
||||
if self._convertWithBlender(filepath, glb_path, progress):
|
||||
@ -102,7 +116,6 @@ class SceneManagerConvertTilesMixin:
|
||||
if progress:
|
||||
progress.setValue(60)
|
||||
progress.setLabelText("尝试 FBX2glTF 转换...")
|
||||
QApplication.processEvents()
|
||||
|
||||
# 方法2: 使用 FBX2glTF (如果可用)
|
||||
if self._convertWithFBX2glTF(filepath, glb_path, progress):
|
||||
@ -111,7 +124,6 @@ class SceneManagerConvertTilesMixin:
|
||||
if progress:
|
||||
progress.setValue(80)
|
||||
progress.setLabelText("尝试 Assimp 转换...")
|
||||
QApplication.processEvents()
|
||||
|
||||
# 方法3: 使用 Assimp
|
||||
if self._convertWithAssimp(filepath, glb_path, progress):
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""Shared tree data-role constants (Qt-independent)."""
|
||||
"""Shared tree data-role constants."""
|
||||
|
||||
# Qt.ItemDataRole.UserRole
|
||||
# Keep this value stable for legacy tree-item data payloads.
|
||||
TREE_USER_ROLE = 256
|
||||
|
||||
|
||||
@ -1,342 +1,154 @@
|
||||
"""
|
||||
图标管理工具
|
||||
|
||||
负责统一管理应用程序中的所有图标:
|
||||
- 图标路径解析
|
||||
- 图标缓存
|
||||
- 图标预加载
|
||||
- 图标错误处理
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Optional
|
||||
from PyQt5.QtGui import QIcon, QPixmap
|
||||
from PyQt5.QtCore import QSize
|
||||
|
||||
|
||||
class IconManager:
|
||||
"""图标管理器类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化图标管理器"""
|
||||
self.icon_cache: Dict[str, QIcon] = {}
|
||||
self.icon_directory = self._get_icon_directory()
|
||||
self.default_icon = None
|
||||
|
||||
# 预定义的图标映射
|
||||
self.icon_map = {
|
||||
# 主窗口图标
|
||||
'app_logo': 'logo.png',
|
||||
|
||||
# 工具栏图标
|
||||
'select_tool': 'select_tool.png',
|
||||
'move_tool': 'move_tool.png',
|
||||
'rotate_tool': 'rotate_tool.png',
|
||||
'scale_tool': 'scale_tool.png',
|
||||
|
||||
# 菜单图标(如果有的话)
|
||||
'new_file': 'new_file.png',
|
||||
'open_file': 'open_file.png',
|
||||
'save_file': 'save_file.png',
|
||||
'exit': 'exit.png',
|
||||
|
||||
# 对象类型图标
|
||||
'object_3d': 'object_3d.png',
|
||||
'light': 'light.png',
|
||||
'camera': 'camera.png',
|
||||
'terrain': 'terrain.png',
|
||||
'script': 'script.png',
|
||||
|
||||
# 状态图标
|
||||
'success': 'success.png',
|
||||
'warning': 'warning.png',
|
||||
'error': 'error.png',
|
||||
'info': 'info.png',
|
||||
|
||||
'up_arrow': 'up_arrows.png',
|
||||
'down_arrow': 'down_arrows.png',
|
||||
'left_arrow': 'left_arrows.png',
|
||||
'right_arrow': 'right_arrows.png',
|
||||
|
||||
'solid_down_arrows': 'solid_down_arrows.png',
|
||||
'solid_right_arrows': 'solid_right_arrows.png',
|
||||
|
||||
'minimize_icon': 'minimize_icon.png',
|
||||
'windowing_icon': 'windowing_icon.png',
|
||||
'close_icon': 'close_icon.png',
|
||||
|
||||
# 弹窗图标
|
||||
'success_icon': 'success_icon.png',
|
||||
'warning_icon': 'warning_icon.png',
|
||||
'fail_icon': 'delete_fail_icon.png',
|
||||
|
||||
# 属性面板图标
|
||||
'property_select_image': 'property_select_image.png',
|
||||
}
|
||||
|
||||
# 初始化默认图标
|
||||
self._create_default_icon()
|
||||
|
||||
# 预加载常用图标
|
||||
self._preload_icons()
|
||||
|
||||
def _get_icon_directory(self) -> str:
|
||||
"""获取图标目录的绝对路径"""
|
||||
# 获取当前文件的目录(ui目录)
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 获取项目根目录(ui的父目录)
|
||||
project_root = os.path.dirname(current_dir)
|
||||
# 拼接icons目录路径
|
||||
icon_dir = os.path.join(project_root, "icons")
|
||||
|
||||
print(f"🔍 图标目录路径: {icon_dir}")
|
||||
|
||||
# 检查目录是否存在
|
||||
if not os.path.exists(icon_dir):
|
||||
print(f"⚠️ 图标目录不存在: {icon_dir}")
|
||||
# 尝试创建目录
|
||||
try:
|
||||
os.makedirs(icon_dir, exist_ok=True)
|
||||
print(f"✅ 已创建图标目录: {icon_dir}")
|
||||
except Exception as e:
|
||||
print(f"❌ 创建图标目录失败: {e}")
|
||||
|
||||
return icon_dir
|
||||
|
||||
def _create_default_icon(self):
|
||||
"""创建默认图标"""
|
||||
# 创建一个简单的默认图标
|
||||
pixmap = QPixmap(16, 16)
|
||||
pixmap.fill() # 填充为白色
|
||||
self.default_icon = QIcon(pixmap)
|
||||
|
||||
def _preload_icons(self):
|
||||
"""预加载常用图标"""
|
||||
print("🔄 开始预加载图标...")
|
||||
|
||||
for icon_name, file_name in self.icon_map.items():
|
||||
icon_path = os.path.join(self.icon_directory, file_name)
|
||||
if os.path.exists(icon_path):
|
||||
try:
|
||||
icon = QIcon(icon_path)
|
||||
self.icon_cache[icon_name] = icon
|
||||
print(f"✅ 已加载图标: {icon_name} -> {file_name}")
|
||||
except Exception as e:
|
||||
print(f"❌ 加载图标失败: {icon_name} -> {file_name}, 错误: {e}")
|
||||
else:
|
||||
print(f"⚠️ 图标文件不存在: {icon_path}")
|
||||
|
||||
print(f"📊 预加载完成,共加载 {len(self.icon_cache)} 个图标")
|
||||
|
||||
def get_icon(self, icon_name: str, size: Optional[QSize] = None) -> QIcon:
|
||||
"""
|
||||
获取图标
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称(可以是预定义名称或文件名)
|
||||
size: 图标尺寸
|
||||
|
||||
Returns:
|
||||
QIcon对象
|
||||
"""
|
||||
# 首先检查缓存
|
||||
if icon_name in self.icon_cache:
|
||||
icon = self.icon_cache[icon_name]
|
||||
if size:
|
||||
# 如果指定了尺寸,返回指定尺寸的图标
|
||||
pixmap = icon.pixmap(size)
|
||||
return QIcon(pixmap)
|
||||
return icon
|
||||
|
||||
# 如果不在缓存中,尝试从映射中获取
|
||||
if icon_name in self.icon_map:
|
||||
file_name = self.icon_map[icon_name]
|
||||
icon_path = os.path.join(self.icon_directory, file_name)
|
||||
else:
|
||||
# 直接使用文件名
|
||||
icon_path = os.path.join(self.icon_directory, icon_name)
|
||||
if not icon_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
|
||||
icon_path += '.png' # 默认添加.png扩展名
|
||||
|
||||
# 尝试加载图标
|
||||
if os.path.exists(icon_path):
|
||||
try:
|
||||
icon = QIcon(icon_path)
|
||||
# 缓存图标
|
||||
self.icon_cache[icon_name] = icon
|
||||
print(f"✅ 动态加载图标: {icon_name} -> {os.path.basename(icon_path)}")
|
||||
|
||||
if size:
|
||||
pixmap = icon.pixmap(size)
|
||||
return QIcon(pixmap)
|
||||
return icon
|
||||
except Exception as e:
|
||||
print(f"❌ 加载图标失败: {icon_path}, 错误: {e}")
|
||||
else:
|
||||
print(f"⚠️ 图标文件不存在: {icon_path}")
|
||||
|
||||
# 返回默认图标
|
||||
return self.default_icon
|
||||
|
||||
def get_icon_path(self, icon_name: str) -> str:
|
||||
"""
|
||||
获取图标文件的完整路径
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称
|
||||
|
||||
Returns:
|
||||
图标文件的完整路径
|
||||
"""
|
||||
if icon_name in self.icon_map:
|
||||
file_name = self.icon_map[icon_name]
|
||||
else:
|
||||
file_name = icon_name
|
||||
if not file_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
|
||||
file_name += '.png'
|
||||
|
||||
icon_path = os.path.join(self.icon_directory, file_name)
|
||||
|
||||
if os.path.exists(icon_path):
|
||||
return icon_path
|
||||
else:
|
||||
print(f"⚠️ 图标文件不存在: {icon_path}")
|
||||
return ""
|
||||
|
||||
def has_icon(self, icon_name: str) -> bool:
|
||||
"""
|
||||
检查图标是否存在
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称
|
||||
|
||||
Returns:
|
||||
是否存在
|
||||
"""
|
||||
return bool(self.get_icon_path(icon_name))
|
||||
|
||||
def add_icon(self, icon_name: str, icon_path: str) -> bool:
|
||||
"""
|
||||
添加新图标到缓存
|
||||
|
||||
Args:
|
||||
icon_name: 图标名称
|
||||
icon_path: 图标文件路径
|
||||
|
||||
Returns:
|
||||
是否添加成功
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(icon_path):
|
||||
icon = QIcon(icon_path)
|
||||
self.icon_cache[icon_name] = icon
|
||||
print(f"✅ 已添加图标到缓存: {icon_name} -> {icon_path}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ 图标文件不存在: {icon_path}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ 添加图标失败: {icon_name} -> {icon_path}, 错误: {e}")
|
||||
return False
|
||||
|
||||
def refresh_cache(self):
|
||||
"""刷新图标缓存"""
|
||||
print("🔄 刷新图标缓存...")
|
||||
self.icon_cache.clear()
|
||||
self._preload_icons()
|
||||
|
||||
def get_available_icons(self) -> list:
|
||||
"""获取所有可用的图标列表"""
|
||||
available_icons = []
|
||||
|
||||
# 添加预定义的图标
|
||||
available_icons.extend(self.icon_map.keys())
|
||||
|
||||
# 扫描图标目录中的所有图标文件
|
||||
if os.path.exists(self.icon_directory):
|
||||
for file_name in os.listdir(self.icon_directory):
|
||||
if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
|
||||
icon_name = os.path.splitext(file_name)[0]
|
||||
if icon_name not in available_icons:
|
||||
available_icons.append(icon_name)
|
||||
|
||||
return sorted(available_icons)
|
||||
|
||||
def get_cache_info(self) -> dict:
|
||||
"""获取缓存信息"""
|
||||
return {
|
||||
'cached_icons': len(self.icon_cache),
|
||||
'icon_directory': self.icon_directory,
|
||||
'available_icons': len(self.get_available_icons()),
|
||||
'cache_keys': list(self.icon_cache.keys())
|
||||
}
|
||||
|
||||
def debug_info(self):
|
||||
"""打印调试信息"""
|
||||
print("=" * 50)
|
||||
print("📋 图标管理器调试信息")
|
||||
print("=" * 50)
|
||||
|
||||
info = self.get_cache_info()
|
||||
print(f"图标目录: {info['icon_directory']}")
|
||||
print(f"目录存在: {os.path.exists(info['icon_directory'])}")
|
||||
print(f"缓存图标数: {info['cached_icons']}")
|
||||
print(f"可用图标数: {info['available_icons']}")
|
||||
|
||||
if info['cache_keys']:
|
||||
print("\n已缓存的图标:")
|
||||
for key in info['cache_keys']:
|
||||
print(f" - {key}")
|
||||
|
||||
print("\n图标目录内容:")
|
||||
if os.path.exists(self.icon_directory):
|
||||
for file_name in os.listdir(self.icon_directory):
|
||||
file_path = os.path.join(self.icon_directory, file_name)
|
||||
size = os.path.getsize(file_path) if os.path.isfile(file_path) else 0
|
||||
print(f" - {file_name} ({size} bytes)")
|
||||
else:
|
||||
print(" 目录不存在")
|
||||
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
# 全局图标管理器实例
|
||||
_icon_manager = None
|
||||
|
||||
|
||||
def get_icon_manager() -> IconManager:
|
||||
"""获取全局图标管理器实例"""
|
||||
global _icon_manager
|
||||
if _icon_manager is None:
|
||||
_icon_manager = IconManager()
|
||||
return _icon_manager
|
||||
|
||||
|
||||
def get_icon(icon_name: str, size: Optional[QSize] = None) -> QIcon:
|
||||
"""便捷函数:获取图标"""
|
||||
return get_icon_manager().get_icon(icon_name, size)
|
||||
|
||||
|
||||
def get_icon_path(icon_name: str) -> str:
|
||||
"""便捷函数:获取图标路径"""
|
||||
return get_icon_manager().get_icon_path(icon_name)
|
||||
|
||||
|
||||
def has_icon(icon_name: str) -> bool:
|
||||
"""便捷函数:检查图标是否存在"""
|
||||
return get_icon_manager().has_icon(icon_name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试代码
|
||||
print("🧪 测试图标管理器...")
|
||||
|
||||
manager = IconManager()
|
||||
manager.debug_info()
|
||||
|
||||
# 测试获取图标
|
||||
logo_icon = manager.get_icon('app_logo')
|
||||
print(f"\n📱 应用图标是否有效: {not logo_icon.isNull()}")
|
||||
|
||||
move_tool_icon = manager.get_icon('move_tool')
|
||||
print(f"🔧 移动工具图标是否有效: {not move_tool_icon.isNull()}")
|
||||
"""
|
||||
Icon manager compatibility layer for the ImGui-only runtime.
|
||||
|
||||
This module keeps the legacy public API so older imports do not break
|
||||
immediately.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IconHandle:
|
||||
"""Lightweight icon placeholder used by legacy call sites."""
|
||||
|
||||
path: str = ""
|
||||
|
||||
def isNull(self) -> bool:
|
||||
return not self.path
|
||||
|
||||
def pixmap(self, _size: Optional[Tuple[int, int]] = None) -> "IconHandle":
|
||||
return self
|
||||
|
||||
|
||||
class IconManager:
|
||||
def __init__(self):
|
||||
self.icon_cache: Dict[str, IconHandle] = {}
|
||||
self.icon_directory = self._get_icon_directory()
|
||||
self.default_icon = IconHandle("")
|
||||
|
||||
self.icon_map = {
|
||||
"app_logo": "logo.png",
|
||||
"select_tool": "select_tool.png",
|
||||
"move_tool": "move_tool.png",
|
||||
"rotate_tool": "rotate_tool.png",
|
||||
"scale_tool": "scale_tool.png",
|
||||
"new_file": "new_file.png",
|
||||
"open_file": "open_file.png",
|
||||
"save_file": "save_file.png",
|
||||
"exit": "exit.png",
|
||||
"object_3d": "object_3d.png",
|
||||
"light": "light.png",
|
||||
"camera": "camera.png",
|
||||
"terrain": "terrain.png",
|
||||
"script": "script.png",
|
||||
"success": "success.png",
|
||||
"warning": "warning.png",
|
||||
"error": "error.png",
|
||||
"info": "info.png",
|
||||
"up_arrow": "up_arrows.png",
|
||||
"down_arrow": "down_arrows.png",
|
||||
"left_arrow": "left_arrows.png",
|
||||
"right_arrow": "right_arrows.png",
|
||||
"solid_down_arrows": "solid_down_arrows.png",
|
||||
"solid_right_arrows": "solid_right_arrows.png",
|
||||
"minimize_icon": "minimize_icon.png",
|
||||
"windowing_icon": "windowing_icon.png",
|
||||
"close_icon": "close_icon.png",
|
||||
"success_icon": "success_icon.png",
|
||||
"warning_icon": "warning_icon.png",
|
||||
"fail_icon": "delete_fail_icon.png",
|
||||
"property_select_image": "property_select_image.png",
|
||||
}
|
||||
|
||||
def _get_icon_directory(self) -> str:
|
||||
current_dir = Path(__file__).resolve().parent
|
||||
project_root = current_dir.parent
|
||||
icon_dir = project_root / "icons"
|
||||
return str(icon_dir)
|
||||
|
||||
def _resolve_icon_path(self, icon_name: str) -> Path:
|
||||
if icon_name in self.icon_map:
|
||||
file_name = self.icon_map[icon_name]
|
||||
else:
|
||||
file_name = icon_name
|
||||
if not file_name.lower().endswith((".png", ".jpg", ".jpeg", ".svg", ".ico")):
|
||||
file_name += ".png"
|
||||
return Path(self.icon_directory) / file_name
|
||||
|
||||
def get_icon(self, icon_name: str, _size: Optional[Tuple[int, int]] = None) -> IconHandle:
|
||||
if icon_name in self.icon_cache:
|
||||
return self.icon_cache[icon_name]
|
||||
|
||||
icon_path = self._resolve_icon_path(icon_name)
|
||||
if icon_path.exists():
|
||||
icon = IconHandle(str(icon_path))
|
||||
self.icon_cache[icon_name] = icon
|
||||
return icon
|
||||
return self.default_icon
|
||||
|
||||
def get_icon_path(self, icon_name: str) -> str:
|
||||
icon_path = self._resolve_icon_path(icon_name)
|
||||
return str(icon_path) if icon_path.exists() else ""
|
||||
|
||||
def has_icon(self, icon_name: str) -> bool:
|
||||
return bool(self.get_icon_path(icon_name))
|
||||
|
||||
def add_icon(self, icon_name: str, icon_path: str) -> bool:
|
||||
path_obj = Path(icon_path)
|
||||
if not path_obj.exists():
|
||||
return False
|
||||
self.icon_cache[icon_name] = IconHandle(str(path_obj))
|
||||
return True
|
||||
|
||||
def refresh_cache(self):
|
||||
self.icon_cache.clear()
|
||||
|
||||
def get_available_icons(self) -> list:
|
||||
available_icons = sorted(self.icon_map.keys())
|
||||
icon_dir = Path(self.icon_directory)
|
||||
if icon_dir.exists():
|
||||
for p in icon_dir.iterdir():
|
||||
if p.suffix.lower() in {".png", ".jpg", ".jpeg", ".svg", ".ico"}:
|
||||
name = p.stem
|
||||
if name not in available_icons:
|
||||
available_icons.append(name)
|
||||
return sorted(available_icons)
|
||||
|
||||
def get_cache_info(self) -> dict:
|
||||
return {
|
||||
"cached_icons": len(self.icon_cache),
|
||||
"icon_directory": self.icon_directory,
|
||||
"available_icons": len(self.get_available_icons()),
|
||||
"cache_keys": list(self.icon_cache.keys()),
|
||||
}
|
||||
|
||||
def debug_info(self):
|
||||
info = self.get_cache_info()
|
||||
print(f"icon_directory: {info['icon_directory']}")
|
||||
print(f"cached_icons: {info['cached_icons']}")
|
||||
print(f"available_icons: {info['available_icons']}")
|
||||
|
||||
|
||||
_icon_manager: Optional[IconManager] = None
|
||||
|
||||
|
||||
def get_icon_manager() -> IconManager:
|
||||
global _icon_manager
|
||||
if _icon_manager is None:
|
||||
_icon_manager = IconManager()
|
||||
return _icon_manager
|
||||
|
||||
|
||||
def get_icon(icon_name: str, size: Optional[Tuple[int, int]] = None) -> IconHandle:
|
||||
return get_icon_manager().get_icon(icon_name, size)
|
||||
|
||||
|
||||
def get_icon_path(icon_name: str) -> str:
|
||||
return get_icon_manager().get_icon_path(icon_name)
|
||||
|
||||
|
||||
def has_icon(icon_name: str) -> bool:
|
||||
return get_icon_manager().has_icon(icon_name)
|
||||
|
||||
4330
ui/widgets.py
4330
ui/widgets.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user