From f1cbd3ffea7fdaaf9be6c5048f8aca4389b1fc4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Mon, 18 Aug 2025 16:10:53 +0800 Subject: [PATCH 1/6] =?UTF-8?q?1.=E4=BF=AE=E6=94=B9=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E6=A8=AA=E5=90=91=E6=98=BE=E7=A4=BA=E5=86=85?= =?UTF-8?q?=E5=AE=B9=202.=E4=BF=AE=E6=94=B9=E9=9D=9E=E9=AA=A8=E9=AA=BC?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E5=B1=9E=E6=80=A7=E9=9D=A2=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/property_panel.py | 62 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/ui/property_panel.py b/ui/property_panel.py index 6905826b..21cf6e1c 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -21,6 +21,19 @@ class PropertyPanelManager: self._propertyLayout = None self._actor_cache={} + # 定义紧凑样式 + self.compact_style = """ + QDoubleSpinBox { + min-width: 45px; + } + QPushButton { + min-width: 10px; + } + QComboBox { + min-width: 50px; + } + """ + def setPropertyLayout(self, layout): """设置属性面板布局引用""" print("开始设置属性布局") @@ -58,6 +71,10 @@ class PropertyPanelManager: self.clearPropertyPanel() + # 应用紧凑样式到属性面板容器 + if self._propertyLayout.parent(): + self._propertyLayout.parent().setStyleSheet(self.compact_style) + itemText = item.text(0) # 如果点击的是场景根节点,显示提示信息 @@ -4939,18 +4956,20 @@ except Exception as e: # 如果有多种类型的动画,使用标签页 if len(animations) > 1: tab_widget = QTabWidget() + tab_widget.setMinimumWidth(200) # 设置最小宽度 for anim_type, anim_data in animations.items(): tab = QWidget() - tab_layout = QVBoxLayout(tab) + tab_layout = QGridLayout(tab) # 改为QGridLayout保持一致 self._buildAnimationTypeUI(tab_layout, origin_model, anim_type, anim_data) tab_widget.addTab(tab, self._getAnimTypeDisplayName(anim_type)) - self._propertyLayout.addRow("动画类型:", tab_widget) + layout.addWidget(QLabel("动画类型:"), 1, 0) + layout.addWidget(tab_widget, 1, 1, 1, 3) else: # 只有一种类型,直接显示 anim_type, anim_data = next(iter(animations.items())) - self._buildAnimationTypeUI(self._propertyLayout, origin_model, anim_type, anim_data) + self._buildAnimationTypeUI(layout, origin_model, anim_type, anim_data) # 存储动画信息供控制使用 if not hasattr(self, '_non_skeletal_cache'): @@ -4961,46 +4980,69 @@ except Exception as e: """为特定动画类型构建UI""" from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox + current_row = layout.rowCount() + if anim_type == 'transform': # 变换动画控制 self.ns_transform_combo = QComboBox() self.ns_transform_combo.addItems(anim_data['names']) - layout.addRow("变换动画:", self.ns_transform_combo) + self.ns_transform_combo.setMinimumWidth(80) + layout.addWidget(QLabel("变换动画:"), current_row, 0) + layout.addWidget(self.ns_transform_combo, current_row, 1, 1, 3) + current_row += 1 elif anim_type == 'texture': # 纹理动画控制 self.ns_texture_combo = QComboBox() self.ns_texture_combo.addItems(anim_data['stages']) - layout.addRow("纹理动画:", self.ns_texture_combo) + self.ns_texture_combo.setMinimumWidth(80) + layout.addWidget(QLabel("纹理动画:"), current_row, 0) + layout.addWidget(self.ns_texture_combo, current_row, 1, 1, 3) + current_row += 1 elif anim_type == 'material': # 材质动画控制 self.ns_material_combo = QComboBox() self.ns_material_combo.addItems(anim_data['properties']) - layout.addRow("材质动画:", self.ns_material_combo) + self.ns_material_combo.setMinimumWidth(80) + layout.addWidget(QLabel("材质动画:"), current_row, 0) + layout.addWidget(self.ns_material_combo, current_row, 1, 1, 3) + current_row += 1 elif anim_type == 'lerp': # Lerp动画控制 self.ns_lerp_combo = QComboBox() self.ns_lerp_combo.addItems(anim_data['intervals']) - layout.addRow("Lerp动画:", self.ns_lerp_combo) + self.ns_lerp_combo.setMinimumWidth(80) + layout.addWidget(QLabel("Lerp动画:"), current_row, 0) + layout.addWidget(self.ns_lerp_combo, current_row, 1, 1, 3) + current_row += 1 # 通用控制按钮 btn_box = QWidget() btn_lay = QHBoxLayout(btn_box) + btn_lay.setContentsMargins(0, 0, 0, 0) for txt, cmd in (("播放", "play"), ("暂停", "pause"), ("停止", "stop"), ("循环", "loop")): btn = QPushButton(txt) + btn.setMinimumWidth(35) # 设置按钮最小宽度 + btn.setMaximumWidth(50) # 限制按钮最大宽度 btn.clicked.connect(lambda _, c=cmd, t=anim_type: self._controlNonSkeletalAnimation(origin_model, t, c)) btn_lay.addWidget(btn) - layout.addRow("控制:", btn_box) + layout.addWidget(QLabel("控制:"), current_row, 0) + layout.addWidget(btn_box, current_row, 1, 1, 3) + current_row += 1 # 播放速度 speed_spinbox = QDoubleSpinBox() speed_spinbox.setRange(0.1, 5.0) speed_spinbox.setSingleStep(0.1) speed_spinbox.setValue(1.0) - speed_spinbox.valueChanged.connect(lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v)) - layout.addRow("播放速度:", speed_spinbox) + speed_spinbox.setMinimumWidth(60) + speed_spinbox.setMaximumWidth(80) + speed_spinbox.valueChanged.connect( + lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v)) + layout.addWidget(QLabel("播放速度:"), current_row, 0) + layout.addWidget(speed_spinbox, current_row, 1, 1, 3) def _getAnimTypeDisplayName(self, anim_type): """获取动画类型的显示名称""" -- 2.45.2 From f41ce004beb30bdb3625faf4d66f90e1b645f667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Mon, 18 Aug 2025 16:14:40 +0800 Subject: [PATCH 2/6] =?UTF-8?q?Revert=20"1.=E4=BF=AE=E6=94=B9=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E9=9D=A2=E6=9D=BF=E6=A8=AA=E5=90=91=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=86=85=E5=AE=B9"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit f1cbd3ffea7fdaaf9be6c5048f8aca4389b1fc4e. --- ui/property_panel.py | 62 +++++++------------------------------------- 1 file changed, 10 insertions(+), 52 deletions(-) diff --git a/ui/property_panel.py b/ui/property_panel.py index 21cf6e1c..6905826b 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -21,19 +21,6 @@ class PropertyPanelManager: self._propertyLayout = None self._actor_cache={} - # 定义紧凑样式 - self.compact_style = """ - QDoubleSpinBox { - min-width: 45px; - } - QPushButton { - min-width: 10px; - } - QComboBox { - min-width: 50px; - } - """ - def setPropertyLayout(self, layout): """设置属性面板布局引用""" print("开始设置属性布局") @@ -71,10 +58,6 @@ class PropertyPanelManager: self.clearPropertyPanel() - # 应用紧凑样式到属性面板容器 - if self._propertyLayout.parent(): - self._propertyLayout.parent().setStyleSheet(self.compact_style) - itemText = item.text(0) # 如果点击的是场景根节点,显示提示信息 @@ -4956,20 +4939,18 @@ except Exception as e: # 如果有多种类型的动画,使用标签页 if len(animations) > 1: tab_widget = QTabWidget() - tab_widget.setMinimumWidth(200) # 设置最小宽度 for anim_type, anim_data in animations.items(): tab = QWidget() - tab_layout = QGridLayout(tab) # 改为QGridLayout保持一致 + tab_layout = QVBoxLayout(tab) self._buildAnimationTypeUI(tab_layout, origin_model, anim_type, anim_data) tab_widget.addTab(tab, self._getAnimTypeDisplayName(anim_type)) - layout.addWidget(QLabel("动画类型:"), 1, 0) - layout.addWidget(tab_widget, 1, 1, 1, 3) + self._propertyLayout.addRow("动画类型:", tab_widget) else: # 只有一种类型,直接显示 anim_type, anim_data = next(iter(animations.items())) - self._buildAnimationTypeUI(layout, origin_model, anim_type, anim_data) + self._buildAnimationTypeUI(self._propertyLayout, origin_model, anim_type, anim_data) # 存储动画信息供控制使用 if not hasattr(self, '_non_skeletal_cache'): @@ -4980,69 +4961,46 @@ except Exception as e: """为特定动画类型构建UI""" from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox - current_row = layout.rowCount() - if anim_type == 'transform': # 变换动画控制 self.ns_transform_combo = QComboBox() self.ns_transform_combo.addItems(anim_data['names']) - self.ns_transform_combo.setMinimumWidth(80) - layout.addWidget(QLabel("变换动画:"), current_row, 0) - layout.addWidget(self.ns_transform_combo, current_row, 1, 1, 3) - current_row += 1 + layout.addRow("变换动画:", self.ns_transform_combo) elif anim_type == 'texture': # 纹理动画控制 self.ns_texture_combo = QComboBox() self.ns_texture_combo.addItems(anim_data['stages']) - self.ns_texture_combo.setMinimumWidth(80) - layout.addWidget(QLabel("纹理动画:"), current_row, 0) - layout.addWidget(self.ns_texture_combo, current_row, 1, 1, 3) - current_row += 1 + layout.addRow("纹理动画:", self.ns_texture_combo) elif anim_type == 'material': # 材质动画控制 self.ns_material_combo = QComboBox() self.ns_material_combo.addItems(anim_data['properties']) - self.ns_material_combo.setMinimumWidth(80) - layout.addWidget(QLabel("材质动画:"), current_row, 0) - layout.addWidget(self.ns_material_combo, current_row, 1, 1, 3) - current_row += 1 + layout.addRow("材质动画:", self.ns_material_combo) elif anim_type == 'lerp': # Lerp动画控制 self.ns_lerp_combo = QComboBox() self.ns_lerp_combo.addItems(anim_data['intervals']) - self.ns_lerp_combo.setMinimumWidth(80) - layout.addWidget(QLabel("Lerp动画:"), current_row, 0) - layout.addWidget(self.ns_lerp_combo, current_row, 1, 1, 3) - current_row += 1 + layout.addRow("Lerp动画:", self.ns_lerp_combo) # 通用控制按钮 btn_box = QWidget() btn_lay = QHBoxLayout(btn_box) - btn_lay.setContentsMargins(0, 0, 0, 0) for txt, cmd in (("播放", "play"), ("暂停", "pause"), ("停止", "stop"), ("循环", "loop")): btn = QPushButton(txt) - btn.setMinimumWidth(35) # 设置按钮最小宽度 - btn.setMaximumWidth(50) # 限制按钮最大宽度 btn.clicked.connect(lambda _, c=cmd, t=anim_type: self._controlNonSkeletalAnimation(origin_model, t, c)) btn_lay.addWidget(btn) - layout.addWidget(QLabel("控制:"), current_row, 0) - layout.addWidget(btn_box, current_row, 1, 1, 3) - current_row += 1 + layout.addRow("控制:", btn_box) # 播放速度 speed_spinbox = QDoubleSpinBox() speed_spinbox.setRange(0.1, 5.0) speed_spinbox.setSingleStep(0.1) speed_spinbox.setValue(1.0) - speed_spinbox.setMinimumWidth(60) - speed_spinbox.setMaximumWidth(80) - speed_spinbox.valueChanged.connect( - lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v)) - layout.addWidget(QLabel("播放速度:"), current_row, 0) - layout.addWidget(speed_spinbox, current_row, 1, 1, 3) + speed_spinbox.valueChanged.connect(lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v)) + layout.addRow("播放速度:", speed_spinbox) def _getAnimTypeDisplayName(self, anim_type): """获取动画类型的显示名称""" -- 2.45.2 From 5665cc90fc9cebea334b9e251b4671f8748b227b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Thu, 21 Aug 2025 10:46:40 +0800 Subject: [PATCH 3/6] =?UTF-8?q?1.=E5=B1=82=E7=BA=A7=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/interface_manager.py | 22 +-- ui/main_window.py | 45 +++++-- ui/property_panel.py | 68 ++++++++-- ui/widgets.py | 292 +++++++++++++++++++++++++++++----------- 4 files changed, 321 insertions(+), 106 deletions(-) diff --git a/ui/interface_manager.py b/ui/interface_manager.py index d45c3458..3a6d22a6 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -133,14 +133,14 @@ class InterfaceManager: cameraItem.setData(0, Qt.UserRole, self.world.cam) print("添加相机节点") - # 添加模型节点组 - modelsItem = QTreeWidgetItem(sceneRoot, ['模型']) - print(f"模型列表中的模型数量: {len(self.world.models)}") - - # 添加GUI元素节点组 - guiItem = QTreeWidgetItem(sceneRoot, ['GUI元素']) - - lightItem = QTreeWidgetItem(sceneRoot,['灯光']) + # # 添加模型节点组 + # modelsItem = QTreeWidgetItem(sceneRoot, ['模型']) + # print(f"模型列表中的模型数量: {len(self.world.models)}") + # + # # 添加GUI元素节点组 + # guiItem = QTreeWidgetItem(sceneRoot, ['GUI元素']) + # + # lightItem = QTreeWidgetItem(sceneRoot,['灯光']) BLACK_LIST = {'','**','temp','collision'} @@ -179,17 +179,17 @@ class InterfaceManager: # print(f"跳过节点: {child.getName()}") for model in self.world.models: - addNodeToTree(model, modelsItem,force=True) + addNodeToTree(model, sceneRoot,force=True) # 添加所有GUI元素 for gui in self.world.gui_elements: gui_type = gui.getTag("gui_type") or "unknown" gui_text = gui.getTag("gui_text") or "GUI元素" - item = QTreeWidgetItem(guiItem, [f"{gui_type}: {gui_text}"]) + item = QTreeWidgetItem(sceneRoot, [f"{gui_type}: {gui_text}"]) item.setData(0, Qt.UserRole, gui) for light in self.world.Spotlight + self.world.Pointlight: - addNodeToTree(light, lightItem, force=True) + addNodeToTree(light, sceneRoot, force=True) # 添加地板节点 if hasattr(self.world, 'ground') and self.world.ground: diff --git a/ui/main_window.py b/ui/main_window.py index a853f494..bd90fd3b 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -92,18 +92,42 @@ class MainWindow(QMainWindow): self.scaleAction = self.toolsMenu.addAction('缩放工具') self.sunsetAction = self.toolsMenu.addAction('光照编辑') self.pluginAction = self.toolsMenu.addAction('图形编辑') - + + # 创建菜单 + self.createMenu = menubar.addMenu('创建') + self.createEnptyaddAction = self.createMenu.addAction('空对象') + self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象') + + self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI') + self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本') + + self.createGUIaddMenu = self.createMenu.addMenu('GUI') + self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮') + self.createLabelAction = self.createGUIaddMenu.addAction('创建标签') + self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框') + self.createGUIaddMenu.addSeparator() + self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕') + + self.createLightaddMenu = self.createMenu.addMenu('光源') + self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯') + self.createPointLightAction = self.createLightaddMenu.addAction('点光源') + # GUI菜单 self.guiMenu = menubar.addMenu('GUI') self.guiEditModeAction = self.guiMenu.addAction('进入GUI编辑模式') self.guiMenu.addSeparator() - self.createButtonAction = self.guiMenu.addAction('创建按钮') - self.createLabelAction = self.guiMenu.addAction('创建标签') - self.createEntryAction = self.guiMenu.addAction('创建输入框') + # self.createButtonAction = self.guiMenu.addAction('创建按钮') + # self.createLabelAction = self.guiMenu.addAction('创建标签') + # self.createEntryAction = self.guiMenu.addAction('创建输入框') + self.guiMenu.addAction(self.createButtonAction) + self.guiMenu.addAction(self.createLabelAction) + self.guiMenu.addAction(self.createEntryAction) self.guiMenu.addSeparator() - self.create3DTextAction = self.guiMenu.addAction('创建3D文本') - self.createVirtualScreenAction = self.guiMenu.addAction('创建虚拟屏幕') - + # self.create3DTextAction = self.guiMenu.addAction('创建3D文本') + self.guiMenu.addAction(self.create3DTextAction) + # self.createVirtualScreenAction = self.guiMenu.addAction('创建虚拟屏幕') + self.guiMenu.addAction(self.createVirtualScreenAction) + # 脚本菜单 self.scriptMenu = menubar.addMenu('脚本') self.createScriptAction = self.scriptMenu.addAction('创建脚本...') @@ -131,6 +155,7 @@ class MainWindow(QMainWindow): # self.leftDock.setMinimumWidth(300) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock) + # 创建右侧停靠窗口(属性窗口) self.rightDock = QDockWidget("属性", self) self.rightDock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) @@ -388,7 +413,11 @@ class MainWindow(QMainWindow): # 连接GUI编辑模式事件 self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode()) - + + # 连接创建事件 + # 连接光源创建按钮事件 + self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight()) + self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight()) # 连接GUI创建按钮事件 self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton()) self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel()) diff --git a/ui/property_panel.py b/ui/property_panel.py index 6905826b..5736cdb0 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -21,6 +21,25 @@ class PropertyPanelManager: self._propertyLayout = None self._actor_cache={} + # 定义紧凑样式 + self.compact_style = """ + QDoubleSpinBox { + min-width: 45px; + } + QPushButton { + min-width: 10px; + } + QComboBox { + min-width: 60px; + } + QLineEdit { + min-width: 60px; + } + QCheckBox { + min-width: 20px; + } + """ + def setPropertyLayout(self, layout): """设置属性面板布局引用""" print("开始设置属性布局") @@ -58,6 +77,10 @@ class PropertyPanelManager: self.clearPropertyPanel() + # 应用紧凑样式到属性面板容器 + if self._propertyLayout.parent(): + self._propertyLayout.parent().setStyleSheet(self.compact_style) + itemText = item.text(0) # 如果点击的是场景根节点,显示提示信息 @@ -4939,18 +4962,20 @@ except Exception as e: # 如果有多种类型的动画,使用标签页 if len(animations) > 1: tab_widget = QTabWidget() + tab_widget.setMinimumWidth(200) # 设置最小宽度 for anim_type, anim_data in animations.items(): tab = QWidget() - tab_layout = QVBoxLayout(tab) + tab_layout = QGridLayout(tab) # 改为QGridLayout保持一致 self._buildAnimationTypeUI(tab_layout, origin_model, anim_type, anim_data) tab_widget.addTab(tab, self._getAnimTypeDisplayName(anim_type)) - self._propertyLayout.addRow("动画类型:", tab_widget) + layout.addWidget(QLabel("动画类型:"), 1, 0) + layout.addWidget(tab_widget, 1, 1, 1, 3) else: # 只有一种类型,直接显示 anim_type, anim_data = next(iter(animations.items())) - self._buildAnimationTypeUI(self._propertyLayout, origin_model, anim_type, anim_data) + self._buildAnimationTypeUI(layout, origin_model, anim_type, anim_data) # 存储动画信息供控制使用 if not hasattr(self, '_non_skeletal_cache'): @@ -4961,46 +4986,69 @@ except Exception as e: """为特定动画类型构建UI""" from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox + current_row = layout.rowCount() + if anim_type == 'transform': # 变换动画控制 self.ns_transform_combo = QComboBox() self.ns_transform_combo.addItems(anim_data['names']) - layout.addRow("变换动画:", self.ns_transform_combo) + self.ns_transform_combo.setMinimumWidth(80) + layout.addWidget(QLabel("变换动画:"), current_row, 0) + layout.addWidget(self.ns_transform_combo, current_row, 1, 1, 3) + current_row += 1 elif anim_type == 'texture': # 纹理动画控制 self.ns_texture_combo = QComboBox() self.ns_texture_combo.addItems(anim_data['stages']) - layout.addRow("纹理动画:", self.ns_texture_combo) + self.ns_texture_combo.setMinimumWidth(80) + layout.addWidget(QLabel("纹理动画:"), current_row, 0) + layout.addWidget(self.ns_texture_combo, current_row, 1, 1, 3) + current_row += 1 elif anim_type == 'material': # 材质动画控制 self.ns_material_combo = QComboBox() self.ns_material_combo.addItems(anim_data['properties']) - layout.addRow("材质动画:", self.ns_material_combo) + self.ns_material_combo.setMinimumWidth(80) + layout.addWidget(QLabel("材质动画:"), current_row, 0) + layout.addWidget(self.ns_material_combo, current_row, 1, 1, 3) + current_row += 1 elif anim_type == 'lerp': # Lerp动画控制 self.ns_lerp_combo = QComboBox() self.ns_lerp_combo.addItems(anim_data['intervals']) - layout.addRow("Lerp动画:", self.ns_lerp_combo) + self.ns_lerp_combo.setMinimumWidth(80) + layout.addWidget(QLabel("Lerp动画:"), current_row, 0) + layout.addWidget(self.ns_lerp_combo, current_row, 1, 1, 3) + current_row += 1 # 通用控制按钮 btn_box = QWidget() btn_lay = QHBoxLayout(btn_box) + btn_lay.setContentsMargins(0, 0, 0, 0) for txt, cmd in (("播放", "play"), ("暂停", "pause"), ("停止", "stop"), ("循环", "loop")): btn = QPushButton(txt) + btn.setMinimumWidth(35) # 设置按钮最小宽度 + btn.setMaximumWidth(50) # 限制按钮最大宽度 btn.clicked.connect(lambda _, c=cmd, t=anim_type: self._controlNonSkeletalAnimation(origin_model, t, c)) btn_lay.addWidget(btn) - layout.addRow("控制:", btn_box) + layout.addWidget(QLabel("控制:"), current_row, 0) + layout.addWidget(btn_box, current_row, 1, 1, 3) + current_row += 1 # 播放速度 speed_spinbox = QDoubleSpinBox() speed_spinbox.setRange(0.1, 5.0) speed_spinbox.setSingleStep(0.1) speed_spinbox.setValue(1.0) - speed_spinbox.valueChanged.connect(lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v)) - layout.addRow("播放速度:", speed_spinbox) + speed_spinbox.setMinimumWidth(60) + speed_spinbox.setMaximumWidth(80) + speed_spinbox.valueChanged.connect( + lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v)) + layout.addWidget(QLabel("播放速度:"), current_row, 0) + layout.addWidget(speed_spinbox, current_row, 1, 1, 3) def _getAnimTypeDisplayName(self, anim_type): """获取动画类型的显示名称""" diff --git a/ui/widgets.py b/ui/widgets.py index 9a1c5dc7..00822588 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -15,7 +15,7 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout, QTreeView, QTreeWidget, QTreeWidgetItem, QWidget, QFileDialog, QMessageBox, QAbstractItemView) from PyQt5.QtCore import Qt, QUrl -from PyQt5.QtGui import QDrag, QPainter, QPixmap +from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush from PyQt5.sip import wrapinstance from QPanda3D.QPanda3DWidget import QPanda3DWidget @@ -319,112 +319,250 @@ class CustomTreeWidget(QTreeWidget): self.setHeaderHidden(True) # 启用多选和拖拽 self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) + self.setDropIndicatorShown(True) # 启用拖放指示线 def setupDragDrop(self): """设置拖拽功能""" # 使用自定义拖拽模式 self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop self.setDefaultDropAction(Qt.DropAction.MoveAction) - + self.setDragEnabled(True) + self.setAcceptDrops(True) + def dropEvent(self, event): - """处理拖放事件""" - # 获取拖动的项和目标项 dragged_item = self.currentItem() target_item = self.itemAt(event.pos()) - + if not dragged_item or not target_item: event.ignore() return - - # 获取节点引用 + + if not self.isValidParentChild(dragged_item, target_item): + event.ignore() + return + dragged_node = dragged_item.data(0, Qt.UserRole) - - # 如果目标是模型根节点,使用 render 作为新父节点 - if target_item.text(0) == "模型": - target_node = self.world.render - else: - target_node = target_item.data(0, Qt.UserRole) - + target_node = target_item.data(0, Qt.UserRole) + if not dragged_node or not target_node: event.ignore() return - - # 检查是否是有效的父子关系 - if self.isValidParentChild(dragged_item, target_item): - # 保存当前的世界坐标 - world_pos = dragged_node.getPos(self.world.render) - - # 更新场景图中的父子关系 - dragged_node.wrtReparentTo(target_node) - - # 接受拖放事件,更新树形控件 - super().dropEvent(event) - - # 更新属性面板 - self.world.updatePropertyPanel(dragged_item) - else: - event.ignore() - + + print(f"dragged_node: {dragged_node}, target_node: {target_node}") + + # 记录拖拽前的父节点 + old_parent_item = dragged_item.parent() + old_parent_node = old_parent_item.data(0, Qt.UserRole) if old_parent_item else None + + # 执行Qt默认拖拽 + super().dropEvent(event) + + # 检查拖拽后的父节点 + new_parent_item = dragged_item.parent() + new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None + + # 同步Panda3D场景图的父子关系 + try: + # 检查是否是跨层级拖拽(父节点发生变化) + 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) + + # 重新父化到新的父节点 + if new_parent_node: + dragged_node.reparentTo(new_parent_node) + else: + # 如果新父节点为None,重新父化到render + dragged_node.reparentTo(self.world.render) + + # 恢复世界坐标位置 + 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) + + event.accept() + + # try: + # world_pos = dragged_node.getPos(self.world.render) + # + # parent_of_dragged = dragged_node.getParent() + # target_node.wrtReparentTo(parent_of_dragged) + # + # # 拖动节点到目标节点下 + # dragged_node.wrtReparentTo(target_node) + # dragged_node.setPos(self.world.render, world_pos) + # + # # 更新 Qt 树控件 + # super().dropEvent(event) + # + # # 更新属性面板 + # self.world.updatePropertyPanel(dragged_item) + # + # event.accept() + # + # except Exception as e: + # print(f"重设父节点失败: {e}") + # event.ignore() + + def _ensureUnderSceneRoot(self, item): + """确保节点在场景根节点下,如果不是则自动修正""" + if not item: + return + + # 检查是否成为了顶级节点 + if not item.parent(): + # 如果节点名称不是"场景",说明意外成为了顶级节点 + if item.text(0) != "场景": + print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...") + + # 找到场景根节点 + scene_root = None + for i in range(self.topLevelItemCount()): + top_item = self.topLevelItem(i) + if top_item.text(0) == "场景": + 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): - """检查是否是有效的父子关系""" - # 不能拖放到自己上 + """检查是否是有效的父子关系(防止循环)""" + + # 1. 禁止拖放到自身 if dragged_item == target_item: return False - - # 不能拖放到自己的子节点上 - parent = target_item - while parent: - if parent == dragged_item: - return False - parent = parent.parent() - - # 检查目标项 - if target_item.text(0) == "场景": - return False # 不能拖放到场景根节点 - - # 允许拖放到模型根节点或其他模型节点 - if target_item.text(0) == "模型": - return True - - # 检查目标项的父节点 - target_parent = target_item.parent() - if not target_parent: + + # 2. 禁止拖到根节点之外(根节点本身除外) + target_root = self._getRootNode(target_item) + if target_root != "场景": + print(f"❌ 目标节点 {target_item.text(0)} 不在场景下") return False - - # 允许在模型节点下的任何位置调整父子关系 - while target_parent: - if target_parent.text(0) == "模型": - return True - target_parent = target_parent.parent() - - return False - + + # 3. 禁止拖拽"场景"根节点 + dragged_root = self._getRootNode(dragged_item) + if dragged_item.text(0) == "场景" or dragged_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() + + return True + + def _getRootNode(self, item): + """获取树中节点的根节点文本""" + current = item + while current.parent(): + current = current.parent() + return current.text(0) + def dragEnterEvent(self, event): """处理拖入事件""" if event.source() == self: event.accept() else: event.ignore() - + def dragMoveEvent(self, event): """处理拖动事件""" - if event.source() == self: - event.accept() - else: + 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("删除完成") + # 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: + # 执行删除操作 + self.delete_items(selected_items) + else: + # 没有选中任何项目,执行默认操作 + super().keyPressEvent(event) else: - super().keyPressEvent(event) \ No newline at end of file + super().keyPressEvent(event) + + def delete_items(self, selected_items): + """删除选中的项目""" + if not selected_items: + return + + # 准备确认对话框的内容 + item_count = len(selected_items) + if item_count == 1: + item_names = f'"{selected_items[0].text(0)}"' + title = "确认删除" + message = f"确定要删除节点 {item_names} 吗?" + else: + item_names = "、".join([f'"{item.text(0)}"' for item in selected_items[:3]]) + if item_count > 3: + item_names += f" 等 {item_count} 个节点" + title = "确认批量删除" + message = f"确定要删除以下 {item_count} 个节点吗?\n\n{item_names}" + + # 创建确认对话框 + reply = QMessageBox.question( + self, + title, + message, + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No # 默认选择"取消",防止误删 + ) + + # 只有用户确认后才执行删除 + if reply == QMessageBox.Yes: + pass + print(f"✅ 已删除 {item_count} 个节点") \ No newline at end of file -- 2.45.2 From 48c90034dfde1400b540c0deb2f0b41c307cc4e7 Mon Sep 17 00:00:00 2001 From: Hector <2055590199@qq.com> Date: Thu, 21 Aug 2025 11:23:47 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E5=90=88=E5=B9=B6=E5=86=B2=E7=AA=81?= =?UTF-8?q?=E8=A7=A3=E5=86=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/property_panel.py | 13 +++++-------- ui/widgets.py | 36 ++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/ui/property_panel.py b/ui/property_panel.py index a9cfb994..d2c51790 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -1,3 +1,4 @@ +from collections import deque from traceback import print_exc from types import new_class from typing import Hashable @@ -77,6 +78,10 @@ class PropertyPanelManager: # 当节点被拖拽后,需要根据新父节点的状态来更新可见性 self._syncEffectiveVisibility(node) + self._syncSceneVisibility() + def _syncSceneVisibility(self): + scene_root = self.world.render + self._syncEffectiveVisibility(scene_root) def updatePropertyPanel(self, item): @@ -4287,14 +4292,6 @@ class PropertyPanelManager: # 忽略 Actor 加载错误,很多模型都不是角色动画 print(f"[信息] 该模型不包含骨骼动画: {actor_error}") - # 只有在没有骨骼动画时才检测非骨骼动画 - if not has_skeletal_anim: - non_skeletal_anims = self._detectNonSkeletalAnimations(origin_model) - if non_skeletal_anims and self._validateNonSkeletalAnimations(origin_model, non_skeletal_anims): - self._buildNonSkeletalUI(origin_model, non_skeletal_anims, animation_layout) - has_animation = True - print(f"[信息] 检测到非骨骼动画: {list(non_skeletal_anims.keys())}") - # 如果都没有动画 if not has_animation: no_anim_label = QLabel("此模型无动画") diff --git a/ui/widgets.py b/ui/widgets.py index 056d04d6..52f97d1d 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -348,26 +348,22 @@ class CustomTreeWidget(QTreeWidget): event.ignore() return - # 检查是否是有效的父子关系 - if self.isValidParentChild(dragged_item, target_item): - # 保存当前的世界坐标 - world_pos = dragged_node.getPos(self.world.render) - - # 更新场景图中的父子关系 - dragged_node.wrtReparentTo(target_node) - - # 接受拖放事件,更新树形控件 - super().dropEvent(event) + # # 检查是否是有效的父子关系 + # if self.isValidParentChild(dragged_item, target_item): + # # 保存当前的世界坐标 + # world_pos = dragged_node.getPos(self.world.render) + # + # # 更新场景图中的父子关系 + # dragged_node.wrtReparentTo(target_node) + # + # # 接受拖放事件,更新树形控件 + # super().dropEvent(event) + # + # #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item) + # # 更新属性面板 + # self.world.updatePropertyPanel(dragged_item) + # self.world.property_panel._syncEffectiveVisibility(dragged_node) - #self.world.property_panel.updateNodeVisibilityAfterDrag(dragged_item) - # 更新属性面板 - self.world.updatePropertyPanel(dragged_item) - self.world.property_panel._syncEffectiveVisibility(dragged_node) - - - - else: - event.ignore() print(f"dragged_node: {dragged_node}, target_node: {target_node}") @@ -416,7 +412,7 @@ class CustomTreeWidget(QTreeWidget): # 事后验证:确保节点仍在"场景"根节点下 self._ensureUnderSceneRoot(dragged_item) - + self.world.property_panel._syncEffectiveVisibility(dragged_node) event.accept() # try: -- 2.45.2 From 8e8564048ef871044f74a5d57d9ecc2a025112ee Mon Sep 17 00:00:00 2001 From: Hector <2055590199@qq.com> Date: Fri, 22 Aug 2025 10:09:33 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E5=9D=90=E6=A0=87=E8=BD=B4=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E6=97=8B=E8=BD=AC=EF=BC=8C=E7=BC=A9=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RenderPipelineFile/config/daytime.yaml | 2 +- core/RotationHandleFull.fbx | Bin 0 -> 56028 bytes core/RotationHandleQuarter.fbx | Bin 0 -> 41244 bytes core/UniformScaleHandle.fbx | Bin 0 -> 25504 bytes core/selection.py | 691 ++++++++++++++++++++----- core/tool_manager.py | 46 -- ui/property_panel.py | 94 +++- 7 files changed, 657 insertions(+), 176 deletions(-) create mode 100755 core/RotationHandleFull.fbx create mode 100755 core/RotationHandleQuarter.fbx create mode 100755 core/UniformScaleHandle.fbx diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index af9861ed..98f9815c 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -17,7 +17,7 @@ control_points: scattering: sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.5000000000]]] + sun_azimuth: [[[0.5000000000,0.9555555556]]] sun_altitude: [[[0.5000000000,1.0000000000]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: diff --git a/core/RotationHandleFull.fbx b/core/RotationHandleFull.fbx new file mode 100755 index 0000000000000000000000000000000000000000..8c42b1b9ef7c216ffba940ca703e1e2f7787d121 GIT binary patch literal 56028 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 Z|LV$r!{&~;_x5Jeivo*>$_1Tj{STND;F16U literal 0 HcmV?d00001 diff --git a/core/UniformScaleHandle.fbx b/core/UniformScaleHandle.fbx new file mode 100755 index 0000000000000000000000000000000000000000..dc0bfb89d913f167483cab3f8572398f04d4f09b GIT binary patch literal 25504 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 literal 0 HcmV?d00001 diff --git a/core/selection.py b/core/selection.py index 22ff8ef5..8cd4e826 100644 --- a/core/selection.py +++ b/core/selection.py @@ -11,7 +11,7 @@ from PIL.ImageChops import lighter from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib, - TransparencyAttrib, Vec4) + TransparencyAttrib, Vec4, CollisionCapsule) from direct.task.TaskManagerGlobal import taskMgr import math @@ -38,6 +38,9 @@ class SelectionSystem: self.gizmoXAxis = None # X轴 self.gizmoYAxis = None # Y轴 self.gizmoZAxis = None # Z轴 + self.gizmoRotXAxis = None + self.gizmoRotYAxis = None + self.gizmoRotZAxis = None self.axis_length = 5.0 # 坐标轴长度(增加到5.0) # 拖拽相关状态 @@ -312,6 +315,8 @@ class SelectionSystem: self._setupGizmoRendering() + self.setupGizmoCollision() + # 现在才显示坐标轴 self.gizmo.show() @@ -329,41 +334,120 @@ class SelectionSystem: if not self.gizmo: return - model_paths = [ - "core/TranslateArrowHandle.fbx", - "EG/core/TranslateArrowHandle.fbx", - ] - arrow_model = None + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False + + if is_scale_tool: + model_paths = [ + "core/UniformScaleHandle.fbx", + ] + elif is_rotate_tool: + model_paths = [ + "core/RotationHandleQuarter.fbx", + ] + else: + model_paths = [ + "core/TranslateArrowHandle.fbx", + ] + + # model_paths = [ + # "core/TranslateArrowHandle.fbx", + # "EG/core/TranslateArrowHandle.fbx", + # ] + gizmo_model = None + gizmoRot_model = None for path in model_paths: try: - arrow_model = self.world.loader.loadModel(path) - if arrow_model: - print(f"成功加载模型: {path}") - break + if is_rotate_tool: + gizmo_model = self.world.loader.loadModel("core/TranslateArrowHandle.fbx") + gizmoRot_model = self.world.loader.loadModel(path) + else: + gizmo_model = self.world.loader.loadModel(path) + if gizmo_model: + print(f"成功加载模型: {path}") + break except: continue + + x_rHandle = None + y_rHandle = None + z_rHandle = None + + if is_rotate_tool: + self.gizmoRotXAxis = self.gizmo.attachNewNode("gizmo_rot_x_axis") + x_rHandle = gizmoRot_model.copyTo(self.gizmoRotXAxis) + x_rHandle.setName("x_handle") + self.gizmoRotYAxis = self.gizmo.attachNewNode("gizmo_rot_y_axis") + y_rHandle = gizmoRot_model.copyTo(self.gizmoRotYAxis) + y_rHandle.setName("y_handle") + self.gizmoRotZAxis = self.gizmo.attachNewNode("gizmo_rot_z_axis") + z_rHandle = gizmoRot_model.copyTo(self.gizmoRotZAxis) + z_rHandle.setName("z_handle") + self.gizmoXAxis = self.gizmo.attachNewNode("gizmo_x_axis") - x_arrow = arrow_model.copyTo(self.gizmoXAxis) - x_arrow.setName("x_arrow") - x_arrow.setHpr(0,-90,0) - x_arrow.setScale(0.1,0.05,0.05) - x_arrow.setPos(0,0,0) + x_handle = gizmo_model.copyTo(self.gizmoXAxis) + x_handle.setName("x_handle") self.gizmoYAxis = self.gizmo.attachNewNode("gizmo_y_axis") - y_arrow = arrow_model.copyTo(self.gizmoYAxis) - y_arrow.setName("y_arrow") - y_arrow.setHpr(90,0,0) - y_arrow.setScale(0.1,0.05,0.05) - y_arrow.setPos(0,0,0) + y_handle = gizmo_model.copyTo(self.gizmoYAxis) + y_handle.setName("y_handle") - # 创建Z轴(蓝色) self.gizmoZAxis = self.gizmo.attachNewNode("gizmo_z_axis") - z_arrow = arrow_model.copyTo(self.gizmoZAxis) - z_arrow.setName("z_arrow") - # 旋转箭头使其指向Z轴正方向 - z_arrow.setHpr(0, 0, -90) # 根据需要调整旋转 - z_arrow.setScale(0.1,0.05,0.05) - z_arrow.setPos(0, 0, 0) + z_handle = gizmo_model.copyTo(self.gizmoZAxis) + z_handle.setName("z_handle") + + if is_scale_tool: + x_handle.setHpr(0,-90,0) + x_handle.setScale(0.6,0.03,0.03) + x_handle.setPos(2.2,0,0) + + y_handle.setHpr(90,0,0) + y_handle.setScale(0.6,0.03,0.03) + y_handle.setPos(0,2.2,0) + + z_handle.setHpr(0,0,-90) + z_handle.setScale(0.6,0.03,0.03) + z_handle.setPos(0,0,2.2) + elif is_rotate_tool: + x_rHandle.setHpr(0,0,90) + x_rHandle.setScale(0.025,0.0125,0.0125) + x_rHandle.setPos(0,0,0) + + y_rHandle.setHpr(0,0,0) + y_rHandle.setScale(0.025,0.0125,0.0125) + y_rHandle.setPos(0,0,0) + + z_rHandle.setHpr(-90,0,0) + z_rHandle.setScale(0.025,0.0125,0.0125) + z_rHandle.setPos(0,0,0) + + x_handle.setHpr(0, -90, 0) + x_handle.setScale(0.1, 0.05, 0.05) + x_handle.setPos(0, 0, 0) + + y_handle.setHpr(90, 0, 0) + y_handle.setScale(0.1, 0.05, 0.05) + y_handle.setPos(0, 0, 0) + + z_handle.setHpr(0, 0, -90) + z_handle.setScale(0.1, 0.05, 0.05) + z_handle.setPos(0, 0, 0) + + self.setGizmoRotAxisColor("x", self.gizmo_colors["x"]) + self.setGizmoRotAxisColor("y", self.gizmo_colors["y"]) + self.setGizmoRotAxisColor("z", self.gizmo_colors["z"]) + else: + x_handle.setHpr(0,-90,0) + x_handle.setScale(0.1,0.05,0.05) + x_handle.setPos(0,0,0) + + y_handle.setHpr(90,0,0) + y_handle.setScale(0.1,0.05,0.05) + y_handle.setPos(0,0,0) + + z_handle.setHpr(0,0,-90) + z_handle.setScale(0.1,0.05,0.05) + z_handle.setPos(0,0,0) # 设置初始颜色 self.setGizmoAxisColor("x", self.gizmo_colors["x"]) @@ -379,6 +463,7 @@ class SelectionSystem: def _setupGizmoRendering(self): try: axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis] + axis_Rotnodes = [self.gizmoRotXAxis, self.gizmoRotYAxis, self.gizmoRotZAxis] for axis_node in axis_nodes: if axis_node: @@ -388,21 +473,45 @@ class SelectionSystem: axis_node.setFogOff() #设置渲染层级,确保大多数对象之前渲染 axis_node.setBin("fixed",30) - axis_node.setDepthWrite(False) - axis_node.setDepthTest(False) + #axis_node.setDepthWrite(False) + #axis_node.setDepthTest(True) + + for axis_rotnode in axis_Rotnodes: + if axis_rotnode: + axis_rotnode.setLightOff() + axis_rotnode.setShaderOff() + axis_rotnode.setFogOff() + axis_rotnode.setBin("fixed",30) + #axis_rotnode.setDepthWrite(False) + #axis_rotnode.setDepthTest(True) + arrow_nodes = [] if self.gizmoXAxis: - x_arrow = self.gizmoXAxis.find("x_arrow") - if x_arrow: - arrow_nodes.append(x_arrow) + x_handle = self.gizmoXAxis.find("x_handle") + if x_handle: + arrow_nodes.append(x_handle) if self.gizmoYAxis: - y_arrow = self.gizmoYAxis.find("y_arrow") - if y_arrow: - arrow_nodes.append(y_arrow) + y_handle = self.gizmoYAxis.find("y_handle") + if y_handle: + arrow_nodes.append(y_handle) if self.gizmoZAxis: - z_arrow = self.gizmoZAxis.find("z_arrow") - if z_arrow: - arrow_nodes.append(z_arrow) + z_handle = self.gizmoZAxis.find("z_handle") + if z_handle: + arrow_nodes.append(z_handle) + + rot_nodes = [] + if self.gizmoRotXAxis: + x_rHandle = self.gizmoRotXAxis.find("x_handle") + if x_rHandle: + rot_nodes.append(x_rHandle) + if self.gizmoRotYAxis: + y_rHandle = self.gizmoRotYAxis.find("y_handle") + if y_rHandle: + rot_nodes.append(y_rHandle) + if self.gizmoRotZAxis: + z_rHandle = self.gizmoRotZAxis.find("z_handle") + if z_rHandle: + rot_nodes.append(z_rHandle) for arrow_node in arrow_nodes: if arrow_node: @@ -410,17 +519,30 @@ class SelectionSystem: arrow_node.setShaderOff() arrow_node.setFogOff() arrow_node.setBin("fixed",31) - arrow_node.setDepthWrite(False) - arrow_node.setDepthTest(False) + #arrow_node.setDepthWrite(False) + #arrow_node.setDepthTest(False) #启用透明度S arrow_node.setTransparency(TransparencyAttrib.MAlpha) + + for rot_node in rot_nodes: + if rot_node: + rot_node.setLightOff() + rot_node.setShaderOff() + rot_node.setFogOff() + rot_node.setBin("fixed",31) + #rot_node.setDepthWrite(False) + #rot_node.setDepthTest(False) + #启用透明度S + rot_node.setTransparency(TransparencyAttrib.MAlpha) + if self.gizmo: self.gizmo.setLightOff() self.gizmo.setShaderOff() self.gizmo.setFogOff() self.gizmo.setBin("fixed",29) - self.gizmo.setDepthWrite(False) - self.gizmo.setDepthTest(False) + # self.gizmo.setDepthWrite(False) + #self.gizmo.setDepthTest(False) + except Exception as e: print(f"设置坐标轴渲染属性失败: {str(e)}") @@ -448,12 +570,49 @@ class SelectionSystem: self.clearGizmo() return task.done + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False + was_scale_tool = getattr(self,'_last_tool_scale_state',False) + was_rotate_tool =getattr(self,'_last_tool_rotate_state',False) + + tool_changed = (is_scale_tool!=was_scale_tool) or (is_rotate_tool != was_rotate_tool) + + if tool_changed: + self._last_tool_scale_state = is_scale_tool + self._last_tool_rotate_state = is_rotate_tool + + if self.gizmoXAxis: + self.gizmoXAxis.removeNode() + self.gizmoXAxis = None + if self.gizmoYAxis: + self.gizmoYAxis.removeNode() + self.gizmoYAxis = None + if self.gizmoZAxis: + self.gizmoZAxis.removeNode() + self.gizmoZAxis = None + if self.gizmoRotXAxis: + self.gizmoRotXAxis.removeNode() + self.gizmoRotXAxis = None + if self.gizmoRotYAxis: + self.gizmoRotYAxis.removeNode() + self.gizmoRotYAxis = None + if self.gizmoRotZAxis: + self.gizmoRotZAxis.removeNode() + self.gizmoRotZAxis = None + + self.createGizmoGeometry() + + self.setGizmoAxisColor("x",self.gizmo_colors["x"]) + self.setGizmoAxisColor("y",self.gizmo_colors["y"]) + self.setGizmoAxisColor("z",self.gizmo_colors["z"]) + + self.setupGizmoCollision() + light_object = self.gizmoTarget.getPythonTag("rp_light_object") if light_object: light_pos = light_object.pos self.gizmo.setPos(light_object.pos) self.gizmoTarget.setPos(light_pos) - else: # 只在必要时更新位置和朝向 self._updateGizmoPositionAndOrientation() @@ -486,13 +645,25 @@ class SelectionSystem: self.gizmo.setPos(center) self._last_gizmo_bounds_update = current_time - # 更新朝向 - parent_node = self.gizmoTarget.getParent() - if parent_node and parent_node != self.world.render: - parent_hpr = parent_node.getHpr() - self.gizmo.setHpr(parent_hpr) + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + + if is_scale_tool: + self.gizmo.setHpr(self.gizmoTarget.getHpr()) else: - self.gizmo.setHpr(0, 0, 0) + parent_node = self.gizmoTarget.getParent() + if parent_node and parent_node != self.world.render: + parent_hpr = parent_node.getHpr() + self.gizmo.setHpr(parent_hpr) + else: + self.gizmo.setHpr(0,0,0) + + # 更新朝向 + # parent_node = self.gizmoTarget.getParent() + # if parent_node and parent_node != self.world.render: + # parent_hpr = parent_node.getHpr() + # self.gizmo.setHpr(parent_hpr) + # else: + # self.gizmo.setHpr(0, 0, 0) def _updateGizmoScreenSize(self): """动态调整坐标轴大小,保持固定的屏幕大小""" @@ -589,6 +760,97 @@ class SelectionSystem: # except: # pass + + def setGizmoRotAxisColor(self, axis, color): + """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" + try: + from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib + + # 获取对应的轴节点 + axis_nodes = { + "x": self.gizmoRotXAxis, + "y": self.gizmoRotYAxis, + "z": self.gizmoRotZAxis + } + + if axis not in axis_nodes or not axis_nodes[axis]: + return + + axis_node = axis_nodes[axis] + + handle_node = None + handle_node = axis_node.find("x_handle") if axis == "x" else handle_node + handle_node = axis_node.find("y_handle") if axis == "y" else handle_node + handle_node = axis_node.find("z_handle") if axis == "z" else handle_node + + #如果找不到特定名称的节点,尝试查找任何子节点 + if not handle_node: + children = axis_node.getChildren() + if children.getNumPath()>0: + handle_node = children[0] + + if not handle_node: + print(f"未找到{axis}轴的处理模型") + return + + # 创建或获取材质 + mat = Material() + + # 设置材质属性 - 使用自发光确保在RenderPipeline下可见 + mat.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) + mat.setDiffuse(Vec4(0, 0, 0, 1)) + #mat.setEmission(Vec4(color[0], color[1], color[2], 1.0)) # 自发光 + mat.setEmission(Vec4(1,1,1,1.0)) # 自发光 + mat.set_roughness(1) + + # 应用材质 + handle_node.setMaterial(mat, 1) + + + # 设置透明度 + if color[3] < 1.0: + handle_node.setTransparency(TransparencyAttrib.MAlpha) + else: + handle_node.setTransparency(TransparencyAttrib.MNone) + + handle_node.setLightOff() # 禁用光照影响 + handle_node.setShaderOff() # 禁用着色器 + handle_node.setFogOff() # 禁用雾效果 + + handle_node.setBin("fixed",31) + #arrow_node.setDepthWrite(False) + #arrow_node.setDepthTest(True) + + # 保存材质引用以便后续修改 + if axis == "x": + self.xMat = mat + elif axis == "y": + self.yMat = mat + elif axis == "z": + self.zMat = mat + + axis_node.setLightOff() + axis_node.setShaderOff() + axis_node.setFogOff() + axis_node.setBin("fixed", 30) + axis_node.setDepthWrite(False) + axis_node.setDepthTest(True) + + except Exception as e: + print(f"设置坐标轴颜色失败: {str(e)}") + # 回退到简单颜色设置 + try: + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis in axis_nodes and axis_nodes[axis]: + axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) + except: + pass + def setGizmoAxisColor(self, axis, color): """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" try: @@ -606,17 +868,19 @@ class SelectionSystem: axis_node = axis_nodes[axis] - # 查找箭头模型节点 - arrow_node = None - if axis == "x": - arrow_node = axis_node.find("x_arrow") - elif axis == "y": - arrow_node = axis_node.find("y_arrow") - elif axis == "z": - arrow_node = axis_node.find("z_arrow") + handle_node = None + handle_node = axis_node.find("x_handle") if axis == "x" else handle_node + handle_node = axis_node.find("y_handle") if axis == "y" else handle_node + handle_node = axis_node.find("z_handle") if axis == "z" else handle_node - if not arrow_node: - print(f"未找到{axis}轴的箭头模型") + #如果找不到特定名称的节点,尝试查找任何子节点 + if not handle_node: + children = axis_node.getChildren() + if children.getNumPath()>0: + handle_node = children[0] + + if not handle_node: + print(f"未找到{axis}轴的处理模型") return # 创建或获取材质 @@ -630,20 +894,20 @@ class SelectionSystem: mat.set_roughness(1) # 应用材质 - arrow_node.setMaterial(mat, 1) + handle_node.setMaterial(mat, 1) # 设置透明度 if color[3] < 1.0: - arrow_node.setTransparency(TransparencyAttrib.MAlpha) + handle_node.setTransparency(TransparencyAttrib.MAlpha) else: - arrow_node.setTransparency(TransparencyAttrib.MNone) + handle_node.setTransparency(TransparencyAttrib.MNone) - arrow_node.setLightOff() # 禁用光照影响 - arrow_node.setShaderOff() # 禁用着色器 - arrow_node.setFogOff() # 禁用雾效果 + handle_node.setLightOff() # 禁用光照影响 + handle_node.setShaderOff() # 禁用着色器 + handle_node.setFogOff() # 禁用雾效果 - arrow_node.setBin("fixed",31) + handle_node.setBin("fixed",31) #arrow_node.setDepthWrite(False) #arrow_node.setDepthTest(True) @@ -1019,8 +1283,9 @@ class SelectionSystem: if not self.gizmo or self.isDraggingGizmo: return - # 使用统一的检测方法 - hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) + # 使用碰撞检测方法 + hoveredAxis = self.detectGizmoAxisWithCollision(mouseX, mouseY) + #hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) # 简化稳定性检测逻辑 if not hasattr(self, '_last_detected_axis'): @@ -1037,6 +1302,11 @@ class SelectionSystem: # 高亮新的轴 if hoveredAxis: self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) + else: + # 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色 + for axis_name in ["x", "y", "z"]: + if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色 + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) self.gizmoHighlightAxis = hoveredAxis @@ -1074,17 +1344,25 @@ class SelectionSystem: return self.isDraggingGizmo = True - # 使用当前高亮的轴,如果有的话 + + # 使用当前高亮的轴,如果有的话;否则使用传入的轴 if self.gizmoHighlightAxis: self.dragGizmoAxis = self.gizmoHighlightAxis else: self.dragGizmoAxis = axis + self.dragStartMousePos = (mouseX, mouseY) # 保存开始拖拽时目标节点的位置和坐标轴的位置 self.gizmoTargetStartPos = self.gizmoTarget.getPos() self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 + # 添加对缩放的支持:保存初始缩放值 + if self.world.tool_manager.isScaleTool(): + self.gizmoTargetStartScale = self.gizmoTarget.getScale() + elif self.world.tool_manager.isRotateTool(): + self.gizmoTargetStartHpr = self.gizmoTarget.getHpr() + # 确保正在拖动的轴保持高亮状态 if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors: # 先将所有轴恢复为正常颜色 @@ -1094,13 +1372,15 @@ class SelectionSystem: # 然后将当前拖动的轴设置为高亮颜色 self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_highlight_colors[self.dragGizmoAxis]) - self.gizmoHighlightAxis = self.dragGizmoAxis elif axis and axis in self.gizmo_colors: for axis_name in self.gizmo_colors.keys(): - self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + if axis_name != axis: + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis]) - self.gizmoHighlightAxis = axis + self.dragGizmoAxis = axis + + self.gizmoHighlightAxis = self.dragGizmoAxis print( f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})") @@ -1130,10 +1410,55 @@ class SelectionSystem: print("拖拽更新失败: 没有坐标轴起始位置") return + is_scale_tool = self.world.tool_manager.isScaleTool() + is_rotate_tool = self.world.tool_manager.isRotateTool() + # 计算鼠标移动距离(屏幕像素) mouseDeltaX = mouseX - self.dragStartMousePos[0] mouseDeltaY = mouseY - self.dragStartMousePos[1] + if is_scale_tool: + scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01 + start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1)) + + target_hpr = self.gizmoTarget.getHpr() + + if self.dragGizmoAxis == "x": + new_scale = Vec3(start_scale.x * scale_factor,start_scale.y,start_scale.z) + elif self.dragGizmoAxis == "y": + new_scale = Vec3(start_scale.x,start_scale.y*scale_factor,start_scale.z) + elif self.dragGizmoAxis == "z": + z_scale_factor = 1.0 - (mouseDeltaX + mouseDeltaY)*0.01 + new_scale = Vec3(start_scale.x,start_scale.y,start_scale.z*z_scale_factor) + else: + new_scale = Vec3(start_scale.x * scale_factor, + start_scale * scale_factor, + start_scale.z * scale_factor) + #应用新缩放值 + self.gizmoTarget.setScale(new_scale) + #实时更新属性面板 + self.world.property_panel.refreshModelValues(self.gizmoTarget) + return + elif is_rotate_tool: + rotation_speed = 0.5 + rotation_amount = (mouseDeltaX + mouseDeltaY) * rotation_speed + start_hpr = getattr(self,'gizmoTargetStartHpr',self.gizmoTarget.getHpr()) + + if self.dragGizmoAxis == "x": + new_hpr = Vec3(start_hpr.x+rotation_amount,start_hpr.y,start_hpr.z) + elif self.dragGizmoAxis == "y": + new_hpr = Vec3(start_hpr.x,start_hpr.y+rotation_amount,start_hpr.z) + elif self.dragGizmoAxis == "z": + new_hpr = Vec3(start_hpr.x,start_hpr.y,start_hpr.z+rotation_amount) + else: + # 默认绕所有轴旋转 + new_hpr = Vec3(start_hpr.x + rotation_amount, + start_hpr.y + rotation_amount, + start_hpr.z + rotation_amount) + self.gizmoTarget.setHpr(new_hpr) + self.world.property_panel.refreshModelValues(self.gizmoTarget) + return + # 使用坐标轴的实际位置而不是目标节点位置来计算屏幕投影 gizmo_world_pos = self.gizmoStartPos @@ -1184,21 +1509,6 @@ class SelectionSystem: axis_start_screen = worldToScreen(gizmo_world_pos) axis_end_world = gizmo_world_pos + world_axis_vector axis_end_screen = worldToScreen(axis_end_world) - #gizmo_screen = worldToScreen(gizmo_world_pos) - #axis_screen = worldToScreen(axis_end) - - # if not gizmo_screen: - # print("拖拽更新失败: 坐标轴中心不在屏幕内") - # return - # if not axis_screen: - # print("拖拽更新失败: 坐标轴端点不在屏幕内") - # return - # - # # 计算轴在屏幕空间的方向向量 - # screen_axis_dir = ( - # axis_screen[0] - gizmo_screen[0], - # axis_screen[1] - gizmo_screen[1] - # ) if not axis_start_screen or not axis_end_screen: print("拖拽更新失败: 无法获取轴线屏幕坐标") @@ -1209,7 +1519,6 @@ class SelectionSystem: axis_end_screen[1] - axis_start_screen[1] ) - # 归一化屏幕轴方向 import math length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) @@ -1253,40 +1562,16 @@ class SelectionSystem: currentPos = self.gizmoTargetStartPos - # scale_adjustment = 1.0 - # if parent_node and parent_node!= self.world.render: - # current_node = parent_node - # total_scale = 1.0 - # while current_node and current_node != self.world.render: - # node_scale = current_node.getScale() - # avg_scale = (node_scale.x+node_scale.y + node_scale.z)/3.0 - # total_scale *= avg_scale - # current_node = current_node.getParent() - # if total_scale>0: - # scale_adjustment = 1.0 / total_scale - # # parent_scale = parent_node.getScale() - # # avg_scale = (parent_scale.x+parent_scale.y+parent_scale.z)/3.0 - # # if avg_scale>0: - # # scale_adjustment = 1.0 / avg_scale - # - # - # fixed_pixel_to_world_ratio = 0.01 # 1像素 = 0.01世界单位 - # scale_factor = fixed_pixel_to_world_ratio * scale_adjustment - # - # movement_distance = projected_distance * scale_factor - # # 获取当前位置并只修改选中轴的坐标 - # currentPos = self.gizmoTargetStartPos - # 根据拖拽的轴,只修改对应的坐标分量 if self.dragGizmoAxis == "x": newPos = Vec3(currentPos.x + movement_distance, currentPos.y, currentPos.z) - print(f"X轴移动:{currentPos.x} -> {newPos.x}") + #print(f"X轴移动:{currentPos.x} -> {newPos.x}") elif self.dragGizmoAxis == "y": newPos = Vec3(currentPos.x, currentPos.y + movement_distance, currentPos.z) - print(f"Y轴移动:{currentPos.y} -> {newPos.y}") + #print(f"Y轴移动:{currentPos.y} -> {newPos.y}") elif self.dragGizmoAxis == "z": newPos = Vec3(currentPos.x, currentPos.y, currentPos.z + movement_distance) - print(f"Z轴移动:{currentPos.z} -> {newPos.z}") + #print(f"Z轴移动:{currentPos.z} -> {newPos.z}") else: print(f"未知轴: {self.dragGizmoAxis}") return @@ -1329,10 +1614,10 @@ class SelectionSystem: def stopGizmoDrag(self): """停止坐标轴拖拽""" print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") - if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors: - self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_colors[self.dragGizmoAxis]) - # 不要将 gizmoHighlightAxis 设置为 None,保持当前高亮轴的状态 - # self.gizmoHighlightAxis = None + + # 恢复所有轴的颜色 + for axis_name in ["x", "y", "z"]: + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) self.isDraggingGizmo = False self.dragGizmoAxis = None @@ -1341,6 +1626,13 @@ class SelectionSystem: self.gizmoTargetStartPos = None self.gizmoStartPos = None + if hasattr(self, 'gizmoTargetStartScale'): + delattr(self, 'gizmoTargetStartScale') + if hasattr(self, 'gizmoTargetStartHpr'): + delattr(self, 'gizmoTargetStartHpr') + + # 重置高亮轴 + self.gizmoHighlightAxis = None # ==================== 选择管理 ==================== def updateSelection(self, nodePath): @@ -1393,3 +1685,164 @@ class SelectionSystem: if self.selectionBoxTarget and self.selectionBoxTarget.isEmpty(): self.clearSelectionBox() + + def setupGizmoCollision(self): + if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis: + return + + # 清除现有的碰撞节点 + for axis_name in ["x", "y", "z"]: + axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") + if axis_node: + # 查找并移除所有现有的碰撞节点 + collision_nodes = axis_node.findAllMatches("**/gizmo_collision_*") + for collision_node in collision_nodes: + collision_node.removeNode() + + # 为每个轴创建碰撞体 + self.createAxisCollision("x", self.gizmoXAxis) + self.createAxisCollision("y", self.gizmoYAxis) + self.createAxisCollision("z", self.gizmoZAxis) + + def createAxisCollision(self, axis_name, axis_node): + # 为单个轴创建碰撞体 + try: + handle_node = axis_node.find(f"{axis_name}_handle") + if not handle_node or handle_node.isEmpty(): + children = axis_node.getChildren() + if children.getNumPaths() > 0: + handle_node = children[0] + else: + print(f"警告: 未找到 {axis_name} 轴的 handle 节点") + return + + collision_node = CollisionNode(f"gizmo_collision_{axis_name}") + collision_node.setIntoCollideMask(BitMask32.bit(1)) # 设置为into对象 + collision_node.setFromCollideMask(BitMask32.allOff()) # 不作为from对象 + + # 调整碰撞尺寸以匹配实际的轴长度和坐标轴缩放 + scale_factor = self.gizmo.getScale().x if self.gizmo else 1.0 + axis_length = 2.0 * scale_factor + radius = 0.3 * scale_factor + + # 根据轴的类型创建合适的碰撞体 + if axis_name == "x": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(axis_length, 0, 0), + radius + ) + collision_node.addSolid(capsule) + elif axis_name == "y": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(0, axis_length, 0), + radius + ) + collision_node.addSolid(capsule) + elif axis_name == "z": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(0, 0, axis_length), + radius + ) + collision_node.addSolid(capsule) + + # 将碰撞节点附加到handle节点,使其与可视化几何体保持一致 + collision_np = handle_node.attachNewNode(collision_node) + + # 设置标签以便识别 + collision_np.setTag("gizmo_axis", axis_name) + collision_np.setTag("pickable", "1") + + collision_np.hide() # 隐藏碰撞体,只用于检测 + + print(f"✓ 成功创建 {axis_name} 轴碰撞体") + + except Exception as e: + print(f"创建{axis_name}轴碰撞体失败: {e}") + import traceback + traceback.print_exc() + + def detectGizmoAxisWithCollision(self, mouseX, mouseY): + # 使用碰撞体检测鼠标是否悬停在坐标轴上 + if not self.gizmo: + return None + + try: + ray = CollisionRay() + + win_width, win_height = self.world.getWindowSize() + + mouse_x_ndc = (mouseX / win_width) * 2.0 - 1.0 + mouse_y_ndc = 1.0 - (mouseY / win_height) * 2.0 + + ray.setFromLens(self.world.cam.node(), mouse_x_ndc, mouse_y_ndc) + + traverser = CollisionTraverser("gizmo_traverser") + handler = CollisionHandlerQueue() + + # 创建射线节点 + ray_node = CollisionNode('mouseRay') + ray_node.addSolid(ray) + ray_node.setFromCollideMask(BitMask32.bit(1)) # 射线作为from对象 + ray_node.setIntoCollideMask(BitMask32.allOff()) # 射线不作为into对象 + ray_np = self.world.render.attachNewNode(ray_node) + + # 为所有轴的碰撞体设置正确的掩码并添加到遍历器 + collision_found = False + for axis_name in ["x", "y", "z"]: + axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") + if axis_node: + collision_node_path = axis_node.find("**/gizmo_collision_*") + if not collision_node_path.isEmpty(): + collision_node = collision_node_path.node() + collision_node.setFromCollideMask(BitMask32.allOff()) # 碰撞体不作为from对象 + collision_node.setIntoCollideMask(BitMask32.bit(1)) # 碰撞体作为into对象 + collision_found = True + + if not collision_found: + ray_np.removeNode() + return None + + # 执行碰撞检测 - 这里是关键修复点 + traverser.addCollider(ray_np, handler) + traverser.traverse(self.world.render) + + ray_np.removeNode() + + # 检查是否有碰撞 + if handler.getNumEntries() > 0: + handler.sortEntries() + closest_entry = handler.getEntry(0) + + # 获取碰撞的对象 + collided_object = closest_entry.getIntoNodePath() + axis_tag = collided_object.getTag("gizmo_axis") + + if axis_tag in ["x", "y", "z"]: + return axis_tag + + return None + + except Exception as e: + print(f"使用碰撞体检测坐标轴失败: {e}") + import traceback + traceback.print_exc() + return None + + def debugGizmoCollision(self): + print("===碰撞体调试信息===") + for axis_name in ["x","y","z"]: + axis_node = getattr(self,f"gizmo{axis_name.upper()}Axis") + if axis_node: + handle_node = axis_node.find(f"{axis_name}_handle") + collision_node = axis_node.find("**/gizmo_collision_*") + print(f"{axis_name.upper()}轴:") + print(f" - 轴节点: {axis_node}") + print(f" - Handle节点: {handle_node}") + print(f" - 碰撞节点: {collision_node}") + if not collision_node.isEmpty(): + print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}") + else: + print(f"{axis_name.upper()}轴节点不存在") \ No newline at end of file diff --git a/core/tool_manager.py b/core/tool_manager.py index c9344f69..4f5f4cdf 100644 --- a/core/tool_manager.py +++ b/core/tool_manager.py @@ -150,49 +150,3 @@ class ToolManager: except Exception as e: print(f"❌ 启动插件配置器失败: {e}") return False - - def cleanup_processes(self): - """清理所有启动的进程""" - try: - # 清理插件配置器进程 - if hasattr(self, '_plugin_configurator_process') and self._plugin_configurator_process: - if self._plugin_configurator_process.poll() is None: - print("🔄 正在关闭插件配置器...") - self._plugin_configurator_process.terminate() - try: - # 等待进程结束,最多等待5秒 - self._plugin_configurator_process.wait(timeout=5) - print("✅ 插件配置器已正常关闭") - except subprocess.TimeoutExpired: - print("⚠️ 插件配置器未响应,强制关闭...") - self._plugin_configurator_process.kill() - self._plugin_configurator_process.wait() - print("✅ 插件配置器已强制关闭") - self._plugin_configurator_process = None - - # 清理材质编辑器进程(如果存在) - if hasattr(self, '_material_editor_process') and self._material_editor_process: - if self._material_editor_process.poll() is None: - print("🔄 正在关闭材质编辑器...") - self._material_editor_process.terminate() - try: - self._material_editor_process.wait(timeout=5) - print("✅ 材质编辑器已正常关闭") - except subprocess.TimeoutExpired: - print("⚠️ 材质编辑器未响应,强制关闭...") - self._material_editor_process.kill() - self._material_editor_process.wait() - print("✅ 材质编辑器已强制关闭") - self._material_editor_process = None - - except Exception as e: - print(f"⚠️ 清理进程时出错: {e}") - - def get_plugin_configurator_status(self): - """获取插件配置器的运行状态""" - if hasattr(self, '_plugin_configurator_process') and self._plugin_configurator_process: - if self._plugin_configurator_process.poll() is None: - return "运行中" - else: - return "已停止" - return "未启动" diff --git a/ui/property_panel.py b/ui/property_panel.py index d2c51790..0421e5b3 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -395,19 +395,27 @@ class PropertyPanelManager: self.scale_y = QDoubleSpinBox() self.scale_z = QDoubleSpinBox() - # 设置缩放控件属性 - for scale_widget in [self.scale_x, self.scale_y, self.scale_z]: - scale_widget.setRange(0.01, 100) - scale_widget.setSingleStep(0.1) + current_scale = model.getScale() - self.scale_x.setValue(model.getScale().getX()) - self.scale_y.setValue(model.getScale().getY()) - self.scale_z.setValue(model.getScale().getZ()) + # 设置缩放控件属性 + for i, (scale_widget, scale_value) in enumerate(zip([self.scale_x, self.scale_y, self.scale_z], + [current_scale.getX(), current_scale.getY(), + current_scale.getZ()])): + scale_widget.setRange(-1000, 1000) + scale_widget.setSingleStep(0.1) + # 如果缩放值为0,设置为一个很小的非零值 + if scale_value == 0: + scale_value = 0.01 if scale_value >= 0 else -0.01 + scale_widget.setValue(scale_value) + + self.scale_x.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_x, value)) + self.scale_y.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_y, value)) + self.scale_z.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_z, value)) # 连接缩放变化事件 - self.scale_x.valueChanged.connect(lambda v: model.setScale(v, model.getScale().getY(), model.getScale().getZ())) - self.scale_y.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), v, model.getScale().getZ())) - self.scale_z.valueChanged.connect(lambda v: model.setScale(model.getScale().getX(), model.getScale().getY(), v)) + self.scale_x.valueChanged.connect(lambda value: self._updateXScale(model, value)) + self.scale_y.valueChanged.connect(lambda value: self._updateYScale(model, value)) + self.scale_z.valueChanged.connect(lambda value: self._updateZScale(model, value)) # 创建并设置 X, Y, Z 标签居中 x_label3 = QLabel("X") @@ -434,6 +442,72 @@ class PropertyPanelManager: # 材质属性组 self._updateModelMaterialPanel(model) + def _onScaleValueChanged(self, scale_widget, value): + """确保缩放值不为0""" + if value == 0: + # 设置为一个很小的非零值,保持原有符号 + if hasattr(scale_widget, 'value') and scale_widget.value() > 0: + scale_widget.setValue(0.01) + else: + scale_widget.setValue(-0.01) + + def _updateXScale(self, model, value): + """更新X轴缩放值""" + # 确保值不为0 + if value == 0: + sender = None + # 通过遍历找到发出信号的控件 + for widget in [self.scale_x, self.scale_y, self.scale_z]: + if widget.value() == value: + sender = widget + break + if sender: + self._onScaleValueChanged(sender, value) + return + + # 更新模型的X轴缩放 + current_scale = model.getScale() + model.setScale(value, current_scale.getY(), current_scale.getZ()) + self.refreshModelValues(model) + + def _updateYScale(self, model, value): + """更新Y轴缩放值""" + # 确保值不为0 + if value == 0: + sender = None + # 通过遍历找到发出信号的控件 + for widget in [self.scale_x, self.scale_y, self.scale_z]: + if widget.value() == value: + sender = widget + break + if sender: + self._onScaleValueChanged(sender, value) + return + + # 更新模型的Y轴缩放 + current_scale = model.getScale() + model.setScale(current_scale.getX(), value, current_scale.getZ()) + self.refreshModelValues(model) + + def _updateZScale(self, model, value): + """更新Z轴缩放值""" + # 确保值不为0 + if value == 0: + sender = None + # 通过遍历找到发出信号的控件 + for widget in [self.scale_x, self.scale_y, self.scale_z]: + if widget.value() == value: + sender = widget + break + if sender: + self._onScaleValueChanged(sender, value) + return + + # 更新模型的Z轴缩放 + current_scale = model.getScale() + model.setScale(current_scale.getX(), current_scale.getY(), value) + self.refreshModelValues(model) + def refreshModelValues(self,nodePath): if not nodePath or self._propertyLayout is None: return -- 2.45.2 From cdf2cd550ea5f020218aa19e49f2d8cb3c0b548d Mon Sep 17 00:00:00 2001 From: Hector <2055590199@qq.com> Date: Mon, 25 Aug 2025 17:41:48 +0800 Subject: [PATCH 6/6] =?UTF-8?q?1.Cesium=20tilesets=202.3D=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/event_handler.py | 8 +- core/selection.py | 98 +++-- gui/gui_manager.py | 769 ++++++++++++++++++++++++++++++++++++++-- main.py | 13 + scene/scene_manager.py | 312 ++++++++++++++++ ui/interface_manager.py | 49 ++- ui/main_window.py | 258 +++++++++++++- ui/property_panel.py | 262 +++++++++++++- 8 files changed, 1693 insertions(+), 76 deletions(-) diff --git a/core/event_handler.py b/core/event_handler.py index 8308734d..ae684c7f 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -398,12 +398,8 @@ class EventHandler: if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo: x = evt.get('x', 0) y = evt.get('y', 0) - # 只在前5次调用时输出调试信息,避免刷屏 - if not hasattr(self.world, '_highlight_debug_count'): - self.world._highlight_debug_count = 0 - if self.world._highlight_debug_count < 5: - print(f"更新坐标轴高亮: 鼠标({x}, {y}), 坐标轴存在={bool(self.world.selection.gizmo)}") - self.world._highlight_debug_count += 1 + # 减少高亮调试输出,只在需要时输出 + # 已静默处理,避免控制台刷屏 self.world.selection.updateGizmoHighlight(x, y) # 调用CoreWorld的父类方法处理基础的相机旋转 diff --git a/core/selection.py b/core/selection.py index 8cd4e826..063239a6 100644 --- a/core/selection.py +++ b/core/selection.py @@ -647,8 +647,11 @@ class SelectionSystem: is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + #安区地更新朝向 + if is_scale_tool: - self.gizmo.setHpr(self.gizmoTarget.getHpr()) + #self.gizmo.setHpr(self.gizmoTarget.getHpr()) + self.gizmo.setQuat(self.gizmoTarget.getQuat(self.world.render)) else: parent_node = self.gizmoTarget.getParent() if parent_node and parent_node != self.world.render: @@ -1184,10 +1187,23 @@ class SelectionSystem: # 获取坐标轴中心的世界坐标 gizmo_world_pos = self.gizmo.getPos(self.world.render) + #获取坐标轴的世界朝向(考虑旋转) + gizmo_world_quat = self.gizmo.getQuat(self.world.render) + + #计算各轴在世界坐标系中的实际方向向量 + x_axis_world = gizmo_world_quat.xform(Vec3(1,0,0)) + y_axis_world = gizmo_world_quat.xform(Vec3(0,1,0)) + z_axis_world = gizmo_world_quat.xform(Vec3(0,0,1)) + + x_end = gizmo_world_pos + x_axis_world * self.axis_length + y_end = gizmo_world_pos + y_axis_world * self.axis_length + z_end = gizmo_world_pos + z_axis_world * self.axis_length + + # 计算各轴端点的世界坐标 - x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) - y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) - z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) + # x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) + # y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) + # z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) # 将3D坐标投影到屏幕坐标 def worldToScreen(worldPos): @@ -1284,8 +1300,8 @@ class SelectionSystem: return # 使用碰撞检测方法 - hoveredAxis = self.detectGizmoAxisWithCollision(mouseX, mouseY) - #hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) + #hoveredAxis = self.detectGizmoAxisWithCollision(mouseX, mouseY) + hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) # 简化稳定性检测逻辑 if not hasattr(self, '_last_detected_axis'): @@ -1348,8 +1364,17 @@ class SelectionSystem: # 使用当前高亮的轴,如果有的话;否则使用传入的轴 if self.gizmoHighlightAxis: self.dragGizmoAxis = self.gizmoHighlightAxis - else: + elif axis and axis in self.gizmo_colors: self.dragGizmoAxis = axis + else: + # 如果没有明确指定轴,尝试通过鼠标位置检测 + self.dragGizmoAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) + + # 如果仍然无法确定拖拽轴,则取消拖拽 + if not self.dragGizmoAxis: + print("开始拖拽失败: 无法确定拖拽轴") + self.isDraggingGizmo = False + return self.dragStartMousePos = (mouseX, mouseY) @@ -1372,15 +1397,15 @@ class SelectionSystem: # 然后将当前拖动的轴设置为高亮颜色 self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_highlight_colors[self.dragGizmoAxis]) - elif axis and axis in self.gizmo_colors: - for axis_name in self.gizmo_colors.keys(): - if axis_name != axis: - self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) - - self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis]) - self.dragGizmoAxis = axis - - self.gizmoHighlightAxis = self.dragGizmoAxis + # elif axis and axis in self.gizmo_colors: + # for axis_name in self.gizmo_colors.keys(): + # if axis_name != axis: + # self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + # + # self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis]) + # self.dragGizmoAxis = axis + # + # self.gizmoHighlightAxis = self.dragGizmoAxis print( f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})") @@ -1480,13 +1505,30 @@ class SelectionSystem: print(f"拖拽更新失败: 未知轴类型 {self.dragGizmoAxis}") return - # 确定轴向量的变换上下文 + world_axis_vector = local_axis_vector + if parent_node and parent_node != self.world.render: - transform_mat = parent_node.getMat(self.world.render) - world_axis_vector = transform_mat.xformVec(local_axis_vector) + try: + if parent_node.getTransform().hasMat(): + transform_mat = parent_node.getMat(self.world.render) + if not transform_mat.isSingular(): + world_axis_vector = transform_mat.xformVec(local_axis_vector) + else: + print("警告: 检测到奇异变换矩阵,使用默认轴向量") + else: + print("警告: 父节点没有有效的变换矩阵,使用默认轴向量") + except Exception as e: + print(f"变换计算出错: {e},使用默认轴向量") else: world_axis_vector = local_axis_vector + # 确定轴向量的变换上下文 + # if parent_node and parent_node != self.world.render: + # transform_mat = parent_node.getMat(self.world.render) + # world_axis_vector = transform_mat.xformVec(local_axis_vector) + # else: + # world_axis_vector = local_axis_vector + #axis_end = gizmo_world_pos + world_axis_vector # 投影到屏幕空间 @@ -1552,10 +1594,20 @@ class SelectionSystem: current_node = self.gizmoTarget.getParent() while current_node and current_node != self.world.render: - node_scale = current_node.getScale() - avg_scale = (node_scale.x+node_scale.y + node_scale.z) / 3.0 - total_scale_factor *= avg_scale - current_node = current_node.getParent() + try: + if not current_node.isEmpty(): + node_scale = current_node.getScale() + if node_scale.x > 0 and node_scale.y >0 and node_scale.z >0 : + avg_scale = (node_scale.x + node_scale.y + node_scale.z)/3.0 + total_scale_factor *= avg_scale + #avg_scale = (node_scale.x+node_scale.y + node_scale.z) / 3.0 + #total_scale_factor *= avg_scale + current_node = current_node.getParent() + else: + break + except: + break + if total_scale_factor > 0: movement_distance = movement_distance / total_scale_factor diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 550f0bb3..0c87a889 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -15,6 +15,14 @@ from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit, QColorDialog, QLabel, QWidget, QGroupBox, QHBoxLayout) from PyQt5.QtGui import QColor from PyQt5.QtCore import Qt +# 尝试导入 QtWebEngineWidgets,如果失败则设置为 None +try: + from PyQt5.QtWebEngineWidgets import QWebEngineView + WEB_ENGINE_AVAILABLE = True +except ImportError: + QWebEngineView = None + WEB_ENGINE_AVAILABLE = False + print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用") class GUIManager: @@ -139,37 +147,159 @@ class GUIManager: print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") return entry - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1): """创建3D空间文本""" - from panda3d.core import TextNode - + from panda3d.core import TextNode,Material,Vec4,ColorAttrib,TransparencyAttrib + textNode = TextNode(f'3d-text-{len(self.gui_elements)}') textNode.setText(text) textNode.setAlign(TextNode.ACenter) if self.world.getChineseFont(): textNode.setFont(self.world.getChineseFont()) - + + textNode.setTextColor(Vec4(1,1,1,1)) + textNodePath = self.world.render.attachNewNode(textNode) textNodePath.setPos(*pos) - textNodePath.setScale(size) - textNodePath.setColor(1, 1, 0, 1) - textNodePath.setBillboardAxis() # 让文本总是面向相机 - + textNodePath.setScale(size,size,size) + #textNodePath.setBillboardAxis() # 让文本总是面向相机 + + # 为3D文本创建默认材质 + material = Material(f"text-material-{len(self.gui_elements)}") + material.setBaseColor(Vec4(1, 1, 1, 1)) # 白色 + material.setDiffuse(Vec4(1, 1, 1, 1)) + material.setAmbient(Vec4(0.5, 0.5, 0.5, 1)) + material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + #material.setEmission(0,0,0,1) + + textNodePath.setMaterial(material, 1) + + textNodePath.setTransparency(TransparencyAttrib.MAlpha) + textNodePath.setAttrib(ColorAttrib.makeFlat(Vec4(1, 1, 1, 1))) + textNodePath.setLightOff() + # 为GUI元素添加标识 textNodePath.setTag("gui_type", "3d_text") textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") textNodePath.setTag("gui_text", text) textNodePath.setTag("is_gui_element", "1") - + + textNodePath.setDepthWrite(True) # 确保深度写入 + textNodePath.setDepthTest(True) # 启用深度测试 + textNodePath.setBin("fixed", 0) # 设置渲染层级,避免被遮挡 + + # if hasattr(self, 'render_pipeline') and self.render_pipeline: + # try: + # self.render_pipeline.set_effect( + # textNodePath, + # "effects/default.yaml", + # { + # "normal_mapping": False, + # "render_gbuffer": False, + # "alpha_testing": True, + # "parallax_mapping": False, + # "render_shadow": False, + # "render_envmap": False + # }, + # 50 + # ) + # except Exception as e: + # print(f"⚠️ PBR效果应用失败: {e}") + self.gui_elements.append(textNodePath) # 安全地调用updateSceneTree if hasattr(self.world, 'updateSceneTree'): self.world.updateSceneTree() - + print(f"✓ 创建3D文本: {text} (世界位置: {pos})") return textNodePath + def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): + from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib + + # 参数类型检查和转换 + if isinstance(size, (list, tuple)): + if len(size) >= 2: + x_size, y_size = float(size[0]), float(size[1]) + else: + x_size = y_size = float(size[0]) if size else 1.0 + else: + x_size = y_size = float(size) + + # 创建卡片 + cm = CardMaker('gui_3d_image') + cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) + + # 创建3D图像节点 + image_node = self.world.render.attachNewNode(cm.generate()) + image_node.setPos(*pos) + + # 设置面向摄像机 + #image_node.setBillboardAxis() # 让图像总是面向相机 + + # 创建支持贴图的材质 + # mat = Material() + # mat.setName("GUI3DImageMaterial") + # color = LColor(1, 1, 1, 1) + # mat.set_base_color(color) + # mat.set_roughness(0.5) + # mat.set_metallic(0.0) + # image_node.set_material(mat) + + # 为3D图像创建独立的材质 + material = Material(f"image-material-{len(self.gui_elements)}") + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setDiffuse(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 + image_node.setMaterial(material, 1) + + image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 如果提供了图像路径,则加载纹理 + if image_path: + self.update3DImageTexture(image_node, image_path) + + # 应用PBR效果(如果可用) + try: + if hasattr(self, 'render_pipeline') and self.render_pipeline: + self.render_pipeline.set_effect( + image_node, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": False, + "render_envmap": True, + "disable_children_effects": True + }, + 50 + ) + print("✓ GUI 3D图像PBR效果已应用") + except Exception as e: + print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") + + # 为GUI元素添加标识(效仿3D文本方法) + image_node.setTag("gui_type", "3d_image") + image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") + if image_path: + image_node.setTag("gui_image_path", image_path) + image_node.setTag("is_gui_element", "1") + + self.gui_elements.append(image_node) + + # 更新场景树 + if hasattr(self.world, 'updateSceneTree'): + self.world.updateSceneTree() + + print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})") + return image_node def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): @@ -262,15 +392,16 @@ class GUIManager: except Exception as e: print(f"删除GUI元素失败: {str(e)}") return False - + + # 在 gui_manager.py 中确保 editGUIElement 方法正确处理文本颜色 def editGUIElement(self, gui_element, property_name, value): """编辑GUI元素属性""" try: from panda3d.core import TextNode - + gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown" print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}") - + if property_name == "text": if gui_type in ["button", "label"]: gui_element['text'] = value @@ -285,7 +416,7 @@ class GUIManager: print(f"成功更新3D文本: {value}") else: print(f"警告: {gui_type}节点类型为{type(gui_element.node())},不是TextNode类型") - + elif gui_type == "virtual_screen": # 对于虚拟屏幕,需要找到TextNode子节点 print(f"虚拟屏幕有 {gui_element.getNumChildren()} 个子节点") @@ -297,36 +428,53 @@ class GUIManager: text_found = True print(f"成功更新虚拟屏幕文本: {value}") break - + if not text_found: print(f"警告: 在{gui_type}中未找到TextNode子节点") - + gui_element.setTag("gui_text", value) - + + elif property_name == "color": # 添加颜色处理 + if isinstance(value, (list, tuple)) and len(value) >= 3: + # 更新材质颜色 + if not gui_element.hasMaterial(): + material = Material(f"text-material-{gui_element.getName()}") + material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + gui_element.setMaterial(material, 1) + else: + material = gui_element.getMaterial() + material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + gui_element.setMaterial(material, 1) + # 更新 TextNode 的文本颜色 + if isinstance(gui_element.node(), TextNode): + gui_element.node().setTextColor( + Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + # if gui_type in ["3d_text", "virtual_screen"]: + # gui_element.setColor(*value) + # elif gui_type in ["button", "label"]: + # gui_element['text_fg'] = value + elif property_name == "position": if isinstance(value, (list, tuple)) and len(value) >= 3: gui_element.setPos(*value[:3]) - + elif property_name == "scale": if isinstance(value, (int, float)): gui_element.setScale(value) elif isinstance(value, (list, tuple)) and len(value) >= 3: gui_element.setScale(*value[:3]) - - elif property_name == "color": - if isinstance(value, (list, tuple)) and len(value) >= 3: - if gui_type in ["button", "label"]: - gui_element['frameColor'] = value - else: - gui_element.setColor(*value) - + print(f"编辑GUI元素 {gui_type}: {property_name} = {value}") return True - + except Exception as e: print(f"编辑GUI元素失败: {str(e)}") + import traceback + traceback.print_exc() return False - + def duplicateGUIElement(self, gui_element): """复制GUI元素""" try: @@ -346,6 +494,9 @@ class GUIManager: self.createGUIEntry(new_pos, gui_text + "_副本") elif gui_type == "3d_text": self.createGUI3DText(new_pos, gui_text + "_副本") + elif gui_type == "3d_image": + image_path = gui_element.getTag("image_path") + self.createGUI3DImage(new_pos,image_path,size=(2,2)) elif gui_type == "virtual_screen": self.createGUIVirtualScreen(new_pos, text=gui_text + "_副本") @@ -581,6 +732,19 @@ class GUIManager: text_font=self.world.getChineseFont() if self.world.getChineseFont() else None ) y_pos -= spacing + + #3D图片工具 + btn_image = DirectButton( + parent = self.guiEditPanel, + text="3D图片", + pos=(0,0,y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["3d_image"], + frameColor=(0.2,0.8,0.8,1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing # 虚拟屏幕工具 btn_screen = DirectButton( @@ -594,6 +758,43 @@ class GUIManager: text_font=self.world.getChineseFont() if self.world.getChineseFont() else None ) y_pos -= spacing + + #Cesium 集成工具 (仅在Webengine 可用时显示) + if WEB_ENGINE_AVAILABLE: + label_cesium = DirectLabel( + parent=self.guiEditPanel, + text="Cesium 集成", + pos=(0, 0, y_pos), + scale=0.04, + text_fg=(1, 1, 0, 1), + frameColor=(0, 0, 0, 0), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= 0.08 + + # 切换 Cesium 视图按钮 + btn_toggle_cesium = DirectButton( + parent=self.guiEditPanel, + text="切换地图视图", + pos=(0, 0, y_pos), + scale=0.04, + command=self.toggleCesiumView, + frameColor=(0.2, 0.8, 0.6, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 刷新 Cesium 视图按钮 + btn_refresh_cesium = DirectButton( + parent=self.guiEditPanel, + text="刷新地图", + pos=(0, 0, y_pos), + scale=0.04, + command=self.refreshCesiumView, + frameColor=(0.6, 0.8, 0.2, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing # 分隔线 y_pos -= 0.1 @@ -741,6 +942,8 @@ class GUIManager: element = self.createGUIEntry(pos, f"输入框_{len(self.gui_elements)}") elif gui_type == "3d_text": element = self.createGUI3DText(pos, f"3D文本_{len(self.gui_elements)}") + elif gui_type == "3d_image": + element = self.createGUI3DImage(pos) elif gui_type == "virtual_screen": element = self.createGUIVirtualScreen(pos, text=f"屏幕_{len(self.gui_elements)}") else: @@ -950,4 +1153,510 @@ class GUIManager: print(f"更新2D GUI位置: {axis}轴 = {value} (屏幕坐标: {gui_element.getPos()})") except Exception as e: - print(f"编辑2D GUI位置失败: {str(e)}") \ No newline at end of file + print(f"编辑2D GUI位置失败: {str(e)}") + + def update3DImageTexture(self, model_nodepath, image_path): + from panda3d.core import Texture + + try: + # 加载新纹理 + new_texture = self.world.loader.loadTexture(image_path) + if new_texture: + # 确保纹理过滤质量 + new_texture.setMagfilter(Texture.FT_linear) + new_texture.setMinfilter(Texture.FT_linear_mipmap_linear) + + # 应用纹理到模型 + model_nodepath.setTexture(new_texture, 1) + + # 更新标签 + model_nodepath.setTag("gui_image_path", image_path) + + # 确保材质设置正确 + if not model_nodepath.has_material(): + from panda3d.core import Material, LColor + mat = Material() + mat.setName(f"image-material-{id(model_nodepath)}") + mat.setBaseColor(LColor(1, 1, 1, 1)) + mat.setDiffuse(LColor(1, 1, 1, 1)) + mat.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + mat.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + mat.setShininess(10.0) + model_nodepath.setMaterial(mat, 1) + + print(f"✅ 3D图像纹理已更新为: {image_path}") + else: + print(f"❌ 无法加载纹理: {image_path}") + except Exception as e: + print(f"❌ 更新纹理时出错: {e}") + + # 替换现有的 createCesiumView 方法 + + def createCesiumView(self, main_window=None): + """创建 Cesium 视图窗口(离线版本)""" + if not WEB_ENGINE_AVAILABLE: + print("❌ 无法创建Cesium视图: Web引擎不可用") + return None + + try: + from PyQt5.QtWebEngineWidgets import QWebEngineView + from PyQt5.QtWidgets import QDockWidget + from PyQt5.QtCore import QUrl + import os + + # 尝试获取主窗口引用 + if main_window is None: + print("🔍 尝试获取主窗口引用...") + + # 检查各种可能的主窗口引用 + if hasattr(self.world, 'interface_manager'): + print(f" - interface_manager 存在: {self.world.interface_manager}") + if hasattr(self.world.interface_manager, 'main_window'): + main_window = self.world.interface_manager.main_window + print(f" - interface_manager.main_window: {main_window}") + + if main_window is None and hasattr(self.world, 'main_window'): + main_window = self.world.main_window + print(f" - world.main_window: {main_window}") + + # 如果仍然没有主窗口,尝试从树形控件获取 + if main_window is None and self.world.treeWidget: + try: + main_window = self.world.treeWidget.window() + print(f" - 从 treeWidget 获取窗口: {main_window}") + except: + pass + + if main_window is None: + print("✗ 无法获取主窗口引用") + return None + else: + print(f"✅ 使用传入的主窗口引用: {main_window}") + + # 检查主窗口是否有效 + if not hasattr(main_window, 'addDockWidget'): + print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法") + return None + + # 检查是否已经存在 Cesium 视图 + for element in self.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + print("⚠ Cesium 视图已经存在") + # 将其前置显示 + element.show() + element.raise_() + return element + + # 创建停靠窗口 + print(f"🔧 创建 Cesium 停靠窗口,父窗口: {main_window}") + cesium_dock = QDockWidget("Cesium 地图视图(离线)", main_window) + cesium_dock.setObjectName("CesiumView") + + # 创建 Web 视图 + self.cesium_view = QWebEngineView() + + # 使用本地 HTML 文件(离线模式) + local_html_path = os.path.abspath("./cesium_offline.html") + if os.path.exists(local_html_path): + print(f"🌐 加载离线 Cesium: file://{local_html_path}") + self.cesium_view.load(QUrl(f"file://{local_html_path}")) + else: + print("⚠️ 离线文件不存在,使用在线版本") + self.cesium_view.load(QUrl("http://localhost:8080/Apps/HelloWorld.html")) + + # 设置内容 + cesium_dock.setWidget(self.cesium_view) + + # 添加到主窗口 + print("📍 将 Cesium 视图添加到主窗口") + main_window.addDockWidget(Qt.RightDockWidgetArea, cesium_dock) + + # 添加到GUI元素列表以便管理 + self.gui_elements.append(cesium_dock) + + print("✓ Cesium 离线视图已创建并集成到项目中") + return cesium_dock + + except Exception as e: + print(f"✗ 创建 Cesium 视图失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def toggleCesiumView(self): + """切换 Cesium 视图显示状态""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法切换 Cesium 视图") + return None + + try: + # 查找现有的 Cesium 视图 + cesium_dock = None + cesium_index = -1 + for i, element in enumerate(self.gui_elements): + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + cesium_dock = element + cesium_index = i + break + + # 如果存在则移除,否则创建 + if cesium_dock: + # 获取主窗口引用以正确移除停靠窗口 + main_window = None + if (hasattr(self.world, 'interface_manager') and + hasattr(self.world.interface_manager, 'main_window') and + self.world.interface_manager.main_window): + main_window = self.world.interface_manager.main_window + elif hasattr(self.world, 'main_window') and self.world.main_window: + main_window = self.world.main_window + + if main_window and hasattr(main_window, 'removeDockWidget'): + main_window.removeDockWidget(cesium_dock) + + # 从列表中移除 + if cesium_index >= 0: + self.gui_elements.pop(cesium_index) + + print("✓ Cesium 视图已隐藏") + return None + else: + return self.createCesiumView() + + except Exception as e: + print(f"✗ 切换 Cesium 视图失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def refreshCesiumView(self): + """刷新 Cesium 视图""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法刷新 Cesium 视图") + return False + + try: + for element in self.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + web_view = element.widget() + if isinstance(web_view, QWebEngineView): + web_view.reload() + print("✓ Cesium 视图已刷新") + return True + print("⚠ 未找到 Cesium 视图") + return False + except Exception as e: + print(f"✗ 刷新 Cesium 视图失败: {str(e)}") + return False + + def updateCesiumURL(self, url): + """更新 Cesium 视图的 URL""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法更新 Cesium URL") + return False + + try: + for element in self.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + web_view = element.widget() + if isinstance(web_view, QWebEngineView): + from PyQt5.QtCore import QUrl + web_view.load(QUrl(url)) + print(f"✓ Cesium URL 已更新为: {url}") + return True + print("⚠ 未找到 Cesium 视图") + return False + except Exception as e: + print(f"✗ 更新 Cesium URL 失败: {str(e)}") + return False + + # 在 GUIManager 类中添加以下方法 + + def addModelToCesium(self, model_id, model_url, longitude, latitude, height=0, scale=1.0): + """向 Cesium 添加模型""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法操作 Cesium") + return False + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return False + + # 转义特殊字符以防止 JavaScript 语法错误 + escaped_model_id = str(model_id).replace("'", "\\'") + escaped_model_url = str(model_url).replace("'", "\\'").replace("\\", "/") + + # 构造 JavaScript 调用 + js_code = f""" + (function() {{ + if (window.CesiumAPI && typeof window.CesiumAPI.addModel === 'function') {{ + try {{ + var result = window.CesiumAPI.addModel( + '{escaped_model_id}', + '{escaped_model_url}', + {{ + longitude: {longitude}, + latitude: {latitude}, + height: {height} + }}, + {scale} + ); + console.log('Cesium 添加模型结果:', result); + return result || {{success: true, message: 'Model added'}}; + }} catch (error) {{ + console.error('JavaScript 错误:', error); + return {{success: false, message: 'JavaScript error: ' + error.message}}; + }} + }} else {{ + console.error('CesiumAPI.addModel 不可用'); + return {{success: false, message: 'CesiumAPI.addModel not available'}}; + }} + }})(); + """ + + # 定义回调函数处理结果 + def handle_result(result): + try: + if isinstance(result, dict): + if result.get('success', False): + print(f"✓ 成功在 Cesium 中添加模型: {model_id}") + else: + print(f"✗ 在 Cesium 中添加模型失败: {result.get('message', 'Unknown error')}") + else: + print(f"✓ 已发送添加模型请求: {model_id}") + except Exception as callback_error: + print(f"✗ 处理回调结果时出错: {callback_error}") + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code, handle_result) + return True + + except Exception as e: + print(f"✗ 添加模型到 Cesium 失败: {e}") + import traceback + traceback.print_exc() + return False + + # 添加新的方法来集成 Panda3D 场景中的 Cesium Tiles + def addCesiumTilesetToScene(self, tileset_name, tileset_url, position=(0, 0, 0)): + """在 Panda3D 场景中添加 Cesium 3D Tiles""" + try: + # 使用场景管理器加载 tileset + tileset_node = self.world.scene_manager.load_cesium_tileset(tileset_url, position) + + if tileset_node: + # 添加到 GUI 元素列表以便管理 + self.gui_elements.append({ + 'type': 'cesium_tileset', + 'name': tileset_name, + 'node': tileset_node, + 'url': tileset_url + }) + + print(f"✓ 在场景中添加 Cesium tileset: {tileset_name}") + return tileset_node + else: + print(f"✗ 在场景中添加 Cesium tileset 失败: {tileset_name}") + return None + + except Exception as e: + print(f"✗ 在场景中添加 Cesium tileset 出错: {e}") + return None + + def removeModelFromCesium(self, model_id): + """从 Cesium 移除模型""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用") + return False + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return False + + # 构造 JavaScript 调用 + js_code = f""" + if (window.CesiumAPI && typeof window.CesiumAPI.removeModel === 'function') {{ + var result = window.CesiumAPI.removeModel('{model_id}'); + result; + }} else {{ + {{success: false, message: 'CesiumAPI.removeModel not available'}}; + }} + """ + + # 定义回调函数处理结果 + def handle_result(result): + if result and isinstance(result, dict): + if result.get('success', False): + print(f"✓ 成功从 Cesium 中移除模型: {model_id}") + else: + print(f"✗ 从 Cesium 中移除模型失败: {result.get('message', 'Unknown error')}") + else: + print(f"✓ 已发送移除模型请求: {model_id} (无法获取详细结果)") + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code, handle_result) + return True + + except Exception as e: + print(f"✗ 从 Cesium 移除模型失败: {e}") + return False + + def updateCesiumModelPosition(self, model_id, longitude, latitude, height=0): + """更新 Cesium 中模型的位置""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用") + return False + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return False + + # 使用更安全的 JavaScript 字符串构造方式 + escaped_model_id = model_id.replace("'", "\\'") + + # 构造 JavaScript 调用 + js_code = f""" + (function() {{ + if (window.CesiumAPI && typeof window.CesiumAPI.updateModelPosition === 'function') {{ + try {{ + var result = window.CesiumAPI.updateModelPosition( + '{escaped_model_id}', + {{ + longitude: {longitude}, + latitude: {latitude}, + height: {height} + }} + ); + return result || {{success: true, message: 'Position updated'}}; + }} catch (error) {{ + return {{success: false, message: 'JavaScript error: ' + error.message}}; + }} + }} else {{ + return {{success: false, message: 'CesiumAPI.updateModelPosition not available'}}; + }} + }})(); + """ + + # 定义回调函数处理结果 + def handle_result(result): + try: + if isinstance(result, dict): + if result.get('success', False): + print(f"✓ 成功更新 Cesium 中模型位置: {model_id}") + else: + print(f"✗ 更新 Cesium 中模型位置失败: {result.get('message', 'Unknown error')}") + else: + print(f"✓ 已发送更新模型位置请求: {model_id}") + except Exception as callback_error: + print(f"✗ 处理回调结果时出错: {callback_error}") + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code, handle_result) + return True + + except Exception as e: + print(f"✗ 更新 Cesium 中模型位置失败: {e}") + return False + + def getAllCesiumModels(self): + """获取 Cesium 中所有模型的列表""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用") + return None + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return None + + # 构造 JavaScript 调用 + js_code = """ + if (window.CesiumAPI && typeof window.CesiumAPI.getAllModels === 'function') { + var result = window.CesiumAPI.getAllModels(); + JSON.stringify(result); + } else { + JSON.stringify({success: false, message: 'CesiumAPI.getAllModels not available'}); + } + """ + + # 定义回调函数处理结果 + def handle_result(result): + try: + if isinstance(result, str): + import json + result = json.loads(result) + + if result and result.get('success', False): + models = result.get('models', []) + print(f"✓ Cesium 中的模型列表: {models}") + return models + else: + print(f"✗ 获取 Cesium 模型列表失败: {result.get('message', 'Unknown error')}") + return [] + except Exception as e: + print(f"✗ 解析 Cesium 模型列表结果失败: {e}") + return [] + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code) + return None # 异步操作,实际结果通过回调处理 + + except Exception as e: + print(f"✗ 获取 Cesium 模型列表失败: {e}") + return None + + # 添加一个便捷方法来加载本地模型文件 + def addLocalModelToCesium(self, model_id, local_model_path, longitude, latitude, height=0, scale=1.0): + """向 Cesium 添加本地模型文件""" + try: + # 将本地路径转换为相对路径或 URL + import os + if os.path.exists(local_model_path): + # 如果 Cesium 服务器可以访问该路径,可以直接使用 + # 否则需要将模型文件放在 Cesium 的静态资源目录中 + model_url = local_model_path.replace('\\', '/') # 确保使用正斜杠 + return self.addModelToCesium(model_id, model_url, longitude, latitude, height, scale) + else: + print(f"✗ 模型文件不存在: {local_model_path}") + return False + except Exception as e: + print(f"✗ 添加本地模型失败: {e}") + return False diff --git a/main.py b/main.py index 065ef800..2ca74757 100644 --- a/main.py +++ b/main.py @@ -183,6 +183,10 @@ class MyWorld(CoreWorld): """创建3D空间文本""" return self.gui_manager.createGUI3DText(pos, text, size) + def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(2,2)): + """创建3D图片""" + return self.gui_manager.createGUI3DImage(pos,text,size) + def createSpotLight(self,pos=(-20,0,5)): """创建聚光灯""" return self.scene_manager.createSpotLight(pos) @@ -673,6 +677,15 @@ class MyWorld(CoreWorld): "streaming_status": self.getALVRStreamingStatus() if self.isALVRConnected() else None } + def loadCesiumTileset(self,tileset_url,position=(0,0,0)): + return self.scene_manager.load_cesium_tileset(tileset_url,position) + + def addCesiumTileset(self,name,url,position=(0,0,0)): + if hasattr(self,'gui_manager') and self.gui_manager: + return self.gui_manager.addCesiumTilesetToScene(name,url,position) + else: + return self.scene_manager.load_cesium_tileset(url,position) + # ==================== 项目管理功能代理 ==================== # 以下函数代理到project_manager模块的对应功能 diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 75671fe2..a3ae792a 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -12,11 +12,57 @@ from panda3d.core import ( MaterialAttrib, ColorAttrib, Point3, CollisionNode, CollisionSphere, BitMask32, TransparencyAttrib,LColor ) +import json +import aiohttp +import asyncio +from pathlib import Path from panda3d.egg import EggData, EggVertexPool from direct.actor.Actor import Actor from QPanda3D.Panda3DWorld import get_render_pipeline from scene import util +class CesiumIntegration: + def __init__(self, scene_manager): + self.scene_manager = scene_manager + self.world = scene_manager.world + self.tilesets = {} + + def add_tileset(self,name,url,position=(0,0,0)): + try: + tileset_node = self.scene_manager.load_cesium_tileset(url,position) + + if tileset_node: + self.tilesets[name] = { + 'node':tileset_node, + 'url':url, + 'position':position + } + print(f"✓ 添加 Cesium tileset: {name}") + return tileset_node + else: + print(f"✗ 添加 Cesium tileset 失败: {name}") + return None + except Exception as e: + print(f"✗ 添加 Cesium tileset 出错: {e}") + return None + + def remove_tileset(self, name): + """移除 tileset""" + if name in self.tilesets: + tileset_info = self.tilesets[name] + tileset_info['node'].removeNode() + del self.tilesets[name] + print(f"✓ 移除 Cesium tileset: {name}") + return True + return False + + def get_tileset(self, name): + """获取 tileset""" + return self.tilesets.get(name, None) + + def list_tilesets(self): + """列出所有 tilesets""" + return list(self.tilesets.keys()) class SceneManager: """场景管理器 - 统一管理场景中的所有元素""" @@ -33,6 +79,8 @@ class SceneManager: self.Spotlight = [] self.Pointlight = [] + self.tilesets = [] #来存储tilesets + self.cesium_integration = CesiumIntegration(self) print("✓ 场景管理系统初始化完成") @@ -1242,3 +1290,267 @@ except Exception as e: print(f"[PyAssimp转换] 转换过程出错: {e}") return False + def load_cesium_tileset(self, tileset_url, position=(0, 0, 0)): + try: + print(f"加载 Cesium 3D Tiles: {tileset_url}") + + # 创建一个容器节点来管理tileset + node_name = f"cesium_tileset_{len(self.tilesets)}" + tileset_node = self.world.render.attachNewNode(node_name) + tileset_node.setPos(*position) + + #添加标签以便场景树识别 + tileset_node.setTag("is_scene_element","1") + tileset_node.setTag("element_type","cesium_tileset") + tileset_node.setTag("tileset_url",tileset_url) + tileset_node.setTag("file",f"tileset_{len(self.tilesets)}") + + # 存储tileset信息 + tileset_info = { + 'url': tileset_url, + 'node': tileset_node, + 'position': position, + 'tiles': {} + } + + self.tilesets.append(tileset_info) + + # 创建一个临时的可视化占位符,让用户能看到节点已添加 + self._create_placeholder_geometry(tileset_node) + + # 异步加载tileset数据 + self._load_tileset_async(tileset_url, tileset_info) + + # 更新场景树 + self.updateSceneTree() + print(f"✓ Cesium 3D Tiles 加载请求已发送") + return tileset_node + + except Exception as e: + print(f"❌ 加载 Cesium 3D Tiles 失败: {e}") + import traceback + traceback.print_exc() + return None + + def _load_tileset_async(self, tileset_url, tileset_info): + """异步加载 tileset 数据""" + + async def load_tileset(): + try: + async with aiohttp.ClientSession() as session: + async with session.get(tileset_url) as response: + if response.status == 200: + tileset_data = await response.json() + self._parse_tileset(tileset_data, tileset_info) + print(f"✓ Tileset 数据加载完成") + else: + print(f"✗ Tileset 加载失败: {response.status}") + except Exception as e: + print(f"✗ Tileset 加载出错: {e}") + + # 在 Panda3D 的任务系统中运行异步任务 + task = asyncio.ensure_future(load_tileset()) + self._current_asyncio_task = task # 保存任务引用 + self.world.taskMgr.add(self._check_async_task, "check_tileset_load", appendTask=True) + + def _check_async_task(self, panda3d_task): + # 检查 asyncio 任务是否完成 + if hasattr(self, '_current_asyncio_task'): + if self._current_asyncio_task.done(): + try: + self._current_asyncio_task.result() + except Exception as e: + print(f"异步任务出错:{e}") + # 返回 Panda3D 任务管理器的完成状态 + return panda3d_task.done # 注意是 done 而不是 DONE + # 返回 Panda3D 任务管理器的继续状态 + return panda3d_task.cont # 注意是 cont 而不是 CONTINUE + + def _parse_tileset(self,tileset_data,tileset_info): + try: + root = tileset_data.get('root',{}) + self._parse_tile(root,tileset_info['node'],tileset_info) + print("✓ Tileset 解析完成") + except Exception as e: + print(f"✗ Tileset 解析出错: {e}") + + def _parse_tile(self, tile_data, parent_node, tileset_info): + try: + # 获取tileID + tile_id = f"tile_{len(tileset_info['tiles'])}" + print(f"创建tile节点: {tile_id}") + # 创建tile节点 + tile_node = parent_node.attachNewNode(tile_id) + + tileset_info['tiles'][tile_id] = { + 'node': tile_node, + 'data': tile_data, + 'loaded': False + } + + # 如果有内容,创建占位几何体 + if 'content' in tile_data: + print(f"为tile {tile_id} 创建几何体") + self._create_tile_geometry(tile_node) + # 递归解析子tiles + children = tile_data.get('children', []) + print(f"Tile {tile_id} 有 {len(children)} 个子节点") + for child_data in children: + self._parse_tile(child_data, tile_node, tileset_info) + except Exception as e: + print(f"✗ Tile 解析出错: {e}") + import traceback + traceback.print_exc() + + def _create_tile_geometry(self,parent_node): + """为 tile 创建占位几何体""" + try: + # 创建一个简单的立方体作为占位符 + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles, GeomNode + + format = GeomVertexFormat.getV3n3c4() + vdata = GeomVertexData('tile_cube', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + color = GeomVertexWriter(vdata, 'color') + + # 定义立方体顶点 + vertices = [ + (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5), + (-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5) + ] + + for vert in vertices: + vertex.addData3f(*vert) + normal.addData3f(0, 0, 1) + color.addData4f(0.2, 0.6, 0.8, 1.0) + + # 创建几何体 + geom = Geom(vdata) + + # 创建面 + prim = GeomTriangles(Geom.UHStatic) + # 底面 + prim.addVertices(0, 1, 2) + prim.addVertices(0, 2, 3) + # 顶面 + prim.addVertices(4, 7, 6) + prim.addVertices(4, 6, 5) + # 前面 + prim.addVertices(0, 4, 5) + prim.addVertices(0, 5, 1) + # 后面 + prim.addVertices(2, 6, 7) + prim.addVertices(2, 7, 3) + # 左面 + prim.addVertices(0, 3, 7) + prim.addVertices(0, 7, 4) + # 右面 + prim.addVertices(1, 5, 6) + prim.addVertices(1, 6, 2) + + prim.closePrimitive() + geom.addPrimitive(prim) + + # 创建几何节点 + geom_node = GeomNode('tile_geometry') + geom_node.addGeom(geom) + + # 添加到场景 + cube_node = parent_node.attachNewNode(geom_node) + cube_node.setScale(1000) # 放大以便观察 + + # 添加材质 + material = Material() + material.setBaseColor((0.2, 0.6, 0.8, 1.0)) + material.setSpecular((0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + cube_node.setMaterial(material) + + except Exception as e: + print(f"✗ 创建 tile 几何体出错: {e}") + + def _create_placeholder_geometry(self, parent_node): + """创建一个简单的占位符几何体,让用户能看到节点""" + try: + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles, GeomNode + + # 创建简单的立方体作为占位符 + format = GeomVertexFormat.getV3n3c4() + vdata = GeomVertexData('placeholder_cube', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + color = GeomVertexWriter(vdata, 'color') + + # 定义立方体顶点(稍微大一些,便于识别) + size = 1.0 + vertices = [ + (-size, -size, -size), (size, -size, -size), (size, size, -size), (-size, size, -size), + (-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size) + ] + + # 使用更鲜明的颜色 + for vert in vertices: + vertex.addData3f(*vert) + normal.addData3f(0, 0, 1) + color.addData4f(0.0, 1.0, 1.0, 1.0) # 青色 + + # 创建几何体 + geom = Geom(vdata) + + # 创建面 + prim = GeomTriangles(Geom.UHStatic) + # 底面 + prim.addVertices(0, 1, 2) + prim.addVertices(0, 2, 3) + # 顶面 + prim.addVertices(4, 7, 6) + prim.addVertices(4, 6, 5) + # 前面 + prim.addVertices(0, 4, 5) + prim.addVertices(0, 5, 1) + # 后面 + prim.addVertices(2, 6, 7) + prim.addVertices(2, 7, 3) + # 左面 + prim.addVertices(0, 3, 7) + prim.addVertices(0, 7, 4) + # 右面 + prim.addVertices(1, 5, 6) + prim.addVertices(1, 6, 2) + + prim.closePrimitive() + geom.addPrimitive(prim) + + # 创建几何节点 + geom_node = GeomNode('tileset_placeholder') + geom_node.addGeom(geom) + + # 添加到场景 + cube_node = parent_node.attachNewNode(geom_node) + cube_node.setScale(5) # 适当大小 + + # 添加材质 + material = Material() + material.setBaseColor((0.0, 1.0, 1.0, 1.0)) # 青色 + material.setSpecular((0.5, 0.5, 0.5, 1.0)) + material.setShininess(32.0) + cube_node.setMaterial(material) + + # 添加标识标签 + cube_node.setTag("element_type", "cesium_placeholder") + + print("✓ 占位符几何体创建完成") + return cube_node + except Exception as e: + print(f"✗ 创建占位符几何体出错: {e}") + import traceback + traceback.print_exc() + return None + + + diff --git a/ui/interface_manager.py b/ui/interface_manager.py index b3fbd8f3..9797f3c2 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -74,6 +74,10 @@ class InterfaceManager: duplicateAction = menu.addAction("复制") duplicateAction.triggered.connect(lambda: self.world.gui_manager.duplicateGUIElement(nodePath)) + elif hasattr(nodePath,'getTag') and nodePath.getTag("element_type") == "cesium_tileset": + deleteAction = menu.addAction("删除 Cesium Tileset") + deleteAction.triggered.connect(lambda:self.deleteCesiumTileset(nodePath,item)) + else: # 为模型节点或其子节点添加删除选项 parentItem = item.parent() @@ -88,6 +92,40 @@ class InterfaceManager: # 显示菜单 menu.exec_(self.treeWidget.viewport().mapToGlobal(position)) + def deleteCesiumTileset(self, nodePath, item): + """删除 Cesium tileset""" + try: + # 从场景中移除 + nodePath.removeNode() + + # 从 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'] == nodePath: + tilesets_to_remove.append(i) + + # 从后往前删除,避免索引问题 + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] + + # 从树形控件中移除 + parentItem = item.parent() + if parentItem: + parentItem.removeChild(item) + + print(f"成功删除 Cesium tileset: {nodePath.getName()}") + + # 清空属性面板和选择框 + self.world.property_panel.clearPropertyPanel() + self.world.selection.updateSelection(None) + + # 更新场景树 + self.updateSceneTree() + + except Exception as e: + print(f"删除 Cesium tileset 失败: {str(e)}") + def isModelOrChild(self, item): """检查是否是模型节点或其子节点""" while item and item.parent(): @@ -215,10 +253,19 @@ class InterfaceManager: gui_text = gui.getTag("gui_text") or "GUI元素" item = QTreeWidgetItem(sceneRoot, [f"{gui_type}: {gui_text}"]) item.setData(0, Qt.UserRole, gui) - + #添加灯光节点 for light in self.world.Spotlight + self.world.Pointlight: addNodeToTree(light, sceneRoot, force=True) + #添加 Cesium tilesets + if hasattr(self.world,'scene_manager') and hasattr(self.world.scene_manager,'tilesets'): + for i , tileset_info in enumerate(self.world.scene_manager.tilesets): + tileset_node = tileset_info['node'] + tileset_url = tileset_info['url'] + tileset_item = QTreeWidgetItem(sceneRoot,[f"Cesium Tileset {i}"]) + tileset_item.setData(0,Qt.UserRole,tileset_node) + addNodeToTree(tileset_node,tileset_item,force=True) + # 添加地板节点 if hasattr(self.world, 'ground') and self.world.ground: groundItem = QTreeWidgetItem(sceneRoot, ['地板']) diff --git a/ui/main_window.py b/ui/main_window.py index bd90fd3b..cf4e5f4d 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -12,7 +12,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout, - QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget) + QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget,QDialog) from PyQt5.QtCore import Qt, QDir, QTimer from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget @@ -107,6 +107,9 @@ class MainWindow(QMainWindow): self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框') self.createGUIaddMenu.addSeparator() self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕') + self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图') + self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图') + self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图') self.createLightaddMenu = self.createMenu.addMenu('光源') self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯') @@ -139,6 +142,10 @@ class MainWindow(QMainWindow): self.toggleHotReloadAction.setChecked(True) # 默认启用 self.scriptMenu.addSeparator() self.openScriptsManagerAction = self.scriptMenu.addAction('脚本管理器') + + self.cesiumMenu = menubar.addMenu('Cesium') + self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles') + self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset) # 帮助菜单 self.helpMenu = menubar.addMenu('帮助') @@ -264,6 +271,10 @@ class MainWindow(QMainWindow): self.create3DTextTool.setText("3D文本") self.toolbar.addWidget(self.create3DTextTool) + self.create3DImageTool = QToolButton() + self.create3DImageTool.setText("3D图片") + self.toolbar.addWidget(self.create3DImageTool) + self.createSpotLight = QToolButton() self.createSpotLight.setText("聚光灯") self.toolbar.addWidget(self.createSpotLight) @@ -272,6 +283,22 @@ class MainWindow(QMainWindow): self.createPointLight.setText("点光灯") self.toolbar.addWidget(self.createPointLight) + # Cesium 工具按钮 + self.cesiumViewTool = QToolButton() + self.cesiumViewTool.setText("地图视图") + self.cesiumViewTool.clicked.connect(self.onCreateCesiumView) + self.toolbar.addWidget(self.cesiumViewTool) + + self.refreshCesiumTool = QToolButton() + self.refreshCesiumTool.setText("刷新地图") + self.refreshCesiumTool.clicked.connect(self.onRefreshCesiumView) + self.toolbar.addWidget(self.refreshCesiumTool) + + self.addModelTool = QToolButton() + self.addModelTool.setText("添加模型") + self.addModelTool.clicked.connect(self.onAddModelClicked) + self.toolbar.addWidget(self.addModelTool) + # 默认选择"选择"工具 self.selectTool.setChecked(True) self.world.setCurrentTool("选择") @@ -425,11 +452,18 @@ class MainWindow(QMainWindow): self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText()) #self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight()) self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) + self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) + self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) + self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) + self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) + self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) + self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) # 连接工具栏GUI创建按钮事件 self.createButtonTool.clicked.connect(lambda: self.world.createGUIButton()) self.createLabelTool.clicked.connect(lambda: self.world.createGUILabel()) self.create3DTextTool.clicked.connect(lambda: self.world.createGUI3DText()) + self.create3DImageTool.clicked.connect(lambda: self.world.createGUI3DImage()) self.createSpotLight.clicked.connect(lambda :self.world.createSpotLight()) self.createPointLight.clicked.connect(lambda :self.world.createPointLight()) @@ -447,7 +481,225 @@ class MainWindow(QMainWindow): self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts) self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) - + + def onCreateCesiumView(self): + if hasattr(self.world,'gui_manager') and self.world.gui_manager: + self.world.gui_manager.createCesiumView() + else: + QMessageBox.warning(self,"错误","GUI管理其不可用") + + def onToggleCesiumView(self): + """切换 Cesium 视图显示状态""" + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.toggleCesiumView() + else: + QMessageBox.warning(self, "错误", "GUI 管理器不可用") + + def onRefreshCesiumView(self): + """刷新 Cesium 视图""" + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.refreshCesiumView() + else: + QMessageBox.warning(self, "错误", "GUI 管理器不可用") + + def onUpdateCesiumURL(self): + """更新 Cesium URL""" + url, ok = QInputDialog.getText(self, "更新 Cesium URL", "输入新的 URL:", + QLineEdit.Normal, "http://localhost:8080/Apps/HelloWorld.html") + if ok and url: + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.updateCesiumURL(url) + else: + QMessageBox.warning(self, "错误", "GUI 管理器不可用") + + def onAddModelClicked(self): + """处理加入模型按钮点击事件""" + # 检查 Cesium 视图是否存在 + cesium_view_exists = False + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + for element in self.world.gui_manager.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + cesium_view_exists = True + break + + if not cesium_view_exists: + reply = QMessageBox.question( + self, + '提示', + 'Cesium 地图视图尚未打开,是否先打开地图视图?', + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes + ) + + if reply == QMessageBox.Yes: + self.onCreateCesiumView() + # 给一点时间让 Cesium 视图加载 + QTimer.singleShot(1000, self.showAddModelDialog) + return + else: + return + + self.showAddModelDialog() + + def showAddModelDialog(self): + """显示添加模型对话框""" + # 打开文件选择对话框 + file_path, _ = QFileDialog.getOpenFileName( + self, + "选择 3D 模型文件", + "", + "3D 模型文件 (*.glb *.gltf *.obj);;所有文件 (*)" + ) + + if file_path: + # 获取模型位置信息 + coords, ok = self.getModelCoordinates() + if ok: + longitude, latitude, height, scale = coords + + # 生成唯一的模型 ID + import uuid + model_id = f"model_{uuid.uuid4().hex[:8]}" + + try: + # 添加模型到 Cesium + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + success = self.world.gui_manager.addModelToCesium( + model_id, + file_path, + longitude, + latitude, + height, + scale + ) + + if success: + QMessageBox.information( + self, + "成功", + f"模型已成功添加到地图!\n模型ID: {model_id}" + ) + else: + QMessageBox.warning( + self, + "失败", + "添加模型失败,请检查控制台输出" + ) + except Exception as e: + QMessageBox.critical( + self, + "错误", + f"添加模型时发生错误:\n{str(e)}" + ) + + def getModelCoordinates(self): + """获取模型坐标信息的对话框""" + # 创建对话框 + dialog = QDialog(self) + dialog.setWindowTitle("设置模型位置") + dialog.setModal(True) + dialog.resize(300, 200) + + layout = QVBoxLayout(dialog) + + # 经度 + lon_layout = QHBoxLayout() + lon_layout.addWidget(QLabel("经度:")) + lon_spin = QDoubleSpinBox() + lon_spin.setRange(-180, 180) + lon_spin.setValue(116.3975) # 默认北京位置 + lon_layout.addWidget(lon_spin) + layout.addLayout(lon_layout) + + # 纬度 + lat_layout = QHBoxLayout() + lat_layout.addWidget(QLabel("纬度:")) + lat_spin = QDoubleSpinBox() + lat_spin.setRange(-90, 90) + lat_spin.setValue(39.9085) # 默认北京位置 + lat_layout.addWidget(lat_spin) + layout.addLayout(lat_layout) + + # 高度 + height_layout = QHBoxLayout() + height_layout.addWidget(QLabel("高度(米):")) + height_spin = QDoubleSpinBox() + height_spin.setRange(-10000, 100000) + height_spin.setValue(0) + height_layout.addWidget(height_spin) + layout.addLayout(height_layout) + + # 缩放 + scale_layout = QHBoxLayout() + scale_layout.addWidget(QLabel("缩放:")) + scale_spin = QDoubleSpinBox() + scale_spin.setRange(0.001, 100000) + scale_spin.setValue(1.0) + scale_spin.setSingleStep(0.1) + scale_layout.addWidget(scale_spin) + layout.addLayout(scale_layout) + + # 按钮 + button_layout = QHBoxLayout() + ok_button = QPushButton("确定") + cancel_button = QPushButton("取消") + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + layout.addLayout(button_layout) + + # 连接信号 + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) + + # 显示对话框 + if dialog.exec_() == QDialog.Accepted: + return ( + lon_spin.value(), + lat_spin.value(), + height_spin.value(), + scale_spin.value() + ), True + else: + return None, False + + def onLoadCesiumTileset(self): + url,ok = QInputDialog.getText( + self, + "加载 Cesium 3D Tiles", + "输入 tileset.json URL:", + QLineEdit.Normal, + "https://assets.ion.cesium.com/96128/tileset.json" + ) + + if ok and url: + try: + # 生成唯一的 tileset 名称 + import uuid + tileset_name = f"tileset_{uuid.uuid4().hex[:8]}" + + # 加载 tileset + if hasattr(self.world, 'addCesiumTileset'): + success = self.world.addCesiumTileset(tileset_name, url, (0, 0, 0)) + if success: + QMessageBox.information( + self, + "成功", + f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}" + ) + else: + QMessageBox.warning( + self, + "失败", + "加载 Cesium 3D Tiles 失败" + ) + except Exception as e: + QMessageBox.critical( + self, + "错误", + f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}" + ) + + def onToolChanged(self, button): """工具切换事件处理""" if button.isChecked(): @@ -457,7 +709,7 @@ class MainWindow(QMainWindow): else: self.world.setCurrentTool(None) print("工具栏: 取消选择工具") - + # ==================== 脚本管理事件处理 ==================== def refreshScriptsList(self): diff --git a/ui/property_panel.py b/ui/property_panel.py index 0421e5b3..f195c5ae 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -141,10 +141,18 @@ class PropertyPanelManager: # 获取节点对象 model = item.data(0, Qt.UserRole) + if model and hasattr(model,'getTag') and model.getTag("element_type") == "cesium_tileset": + self._showCesiumTilesetProperties(model,item) # 检查是否是GUI元素 - if model and hasattr(model, 'getTag') and model.getTag("gui_type"): - self.updateGUIPropertyPanel(model) - pass + elif model and hasattr(model, 'getTag') and model.getTag("gui_type"): + # gui_type = model.getTag("gui_type") + # if gui_type == "3d_image": + # self._updateGUIImagePropertyPanel(model) + # else: + # self.updateGUIPropertyPanel(model) + # pass + self.updateGUIPropertyPanel(model) + pass elif model and hasattr(model, 'getTag') and model.getTag("light_type"): self.updateLightPropertyPanel(model) pass @@ -552,7 +560,6 @@ class PropertyPanelManager: spin.blockSignals(False) - def updateGUIPropertyPanel(self, gui_element): """更新GUI元素属性面板""" gui_type = gui_element.getTag("gui_type") @@ -568,6 +575,7 @@ class PropertyPanelManager: # typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;") gui_info_layout.addWidget(typeValue, 0, 1) + # 修改 updateGUIPropertyPanel 中的文本属性部分 # 文本属性(如果适用) if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: gui_info_layout.addWidget(QLabel("文本:"), 1, 0) @@ -578,7 +586,8 @@ class PropertyPanelManager: success = self.world.gui_manager.editGUIElement(gui_element, "text", text) if success: # 更新场景树显示的名称 - self.world.scene_manager.updateSceneTree() + if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + self.world.scene_manager.updateSceneTree() textEdit.textChanged.connect(updateText) gui_info_layout.addWidget(textEdit, 1, 1) @@ -676,7 +685,6 @@ class PropertyPanelManager: [pos.getX(), pos.getY(), v])) transform_layout.addWidget(zPos, 1, 3) - # 缩放属性 if hasattr(gui_element, 'getScale'): scale = gui_element.getScale() @@ -684,17 +692,51 @@ class PropertyPanelManager: transform_layout.addWidget(QLabel("缩放"), row_offset, 0) - scaleSpinBox = QDoubleSpinBox() - scaleSpinBox.setRange(0.01, 10) - scaleSpinBox.setSingleStep(0.1) - scaleSpinBox.setValue(scale.getX()) - scaleSpinBox.valueChanged.connect( - lambda v: self.world.gui_manager.editGUIElement(gui_element, "scale", v)) - transform_layout.addWidget(scaleSpinBox, row_offset, 1) + # X缩放 + transform_layout.addWidget(QLabel("长:"), row_offset, 1) + scaleXSpinBox = QDoubleSpinBox() + scaleXSpinBox.setRange(0.01, 1000) + scaleXSpinBox.setSingleStep(0.1) + scaleXSpinBox.setValue(scale.getX()) + scaleXSpinBox.valueChanged.connect(lambda v: self._onScaleValueChanged(scaleXSpinBox, v)) + scaleXSpinBox.valueChanged.connect(lambda v: self._updateGUIScaleX(gui_element, v)) + transform_layout.addWidget(scaleXSpinBox, row_offset, 2) + + row_offset += 1 + transform_layout.addWidget(QLabel("宽:"), row_offset, 1) + scaleYSpinBox = QDoubleSpinBox() + scaleYSpinBox.setRange(0.01, 1000) + scaleYSpinBox.setSingleStep(0.1) + scaleYSpinBox.setValue(scale.getY()) + scaleYSpinBox.valueChanged.connect(lambda v: self._onScaleValueChanged(scaleYSpinBox, v)) + scaleYSpinBox.valueChanged.connect(lambda v: self._updateGUIScaleZ(gui_element, v)) + transform_layout.addWidget(scaleYSpinBox, row_offset, 2) + + # scaleSpinBox = QDoubleSpinBox() + # scaleSpinBox.setRange(0.01, 10) + # scaleSpinBox.setSingleStep(0.1) + # scaleSpinBox.setValue(scale.getX()) + # scaleSpinBox.valueChanged.connect( + # lambda v: self.world.gui_manager.editGUIElement(gui_element, "scale", v)) + # transform_layout.addWidget(scaleSpinBox, row_offset, 1) transform_group.setLayout(transform_layout) self._propertyLayout.addWidget(transform_group) + # 外观属性组 - 添加字体颜色选择 + if gui_type in ["button", "label", "3d_text"]: + appearance_group = QGroupBox("外观属性") + appearance_layout = QGridLayout() + + # 字体颜色选择 + appearance_layout.addWidget(QLabel("字体颜色:"), 0, 0) + colorButton = QPushButton("选择颜色") + colorButton.clicked.connect(lambda checked, elem=gui_element: self._selectGUIColor(elem)) + appearance_layout.addWidget(colorButton, 0, 1) + + appearance_group.setLayout(appearance_layout) + self._propertyLayout.addWidget(appearance_group) + # 外观属性组 if gui_type in ["button", "label"]: appearance_group = QGroupBox("外观属性") @@ -709,6 +751,200 @@ class PropertyPanelManager: appearance_group.setLayout(appearance_layout) self._propertyLayout.addWidget(appearance_group) + if gui_type == "3d_image": + image_group = QGroupBox("图片设置") + image_layout = QGridLayout() + + # 当前图片路径标签 + current_image_label = QLabel("当前图片:") + image_layout.addWidget(current_image_label, 0, 0) + + # 显示当前贴图路径(简化显示) + current_texture_path = gui_element.getTag("texture_path") or "未设置" + texture_label = QLabel(current_texture_path) + texture_label.setWordWrap(True) + image_layout.addWidget(texture_label, 0, 1) + + # 选择图片按钮 + select_texture_button = QPushButton("选择图片...") + image_layout.addWidget(select_texture_button, 1, 0, 1, 2) + + def onSelectTexture(): + from PyQt5.QtWidgets import QFileDialog + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择图片", + "", + "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" + ) + if file_path: + # 应用新纹理到 3D Image + success = self.world.gui_manager.update3DImageTexture(gui_element, file_path) + if success: + # 保存路径到 Tag + gui_element.setTag("texture_path", file_path) + # 更新显示 + texture_label.setText(file_path) + # 可选:刷新场景树或其他 UI + self.world.scene_manager.updateSceneTree() + + select_texture_button.clicked.connect(onSelectTexture) + + image_group.setLayout(image_layout) + self._propertyLayout.addWidget(image_group) + + # 添加弹性空间 + self._propertyLayout.addStretch() + + # 强制更新布局 + if self._propertyLayout: + self._propertyLayout.update() + propertyWidget = self._propertyLayout.parentWidget() + if propertyWidget: + propertyWidget.update() + + def _selectGUIColor(self, gui_element): + """选择GUI元素的字体颜色""" + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + from panda3d.core import Vec4 + + # 获取当前颜色(如果已设置) + current_color = QColor(255, 255, 255) # 默认白色 + + # 尝试获取当前设置的颜色 + gui_type = gui_element.getTag("gui_type") + try: + if gui_type == "3d_text": + if gui_element.hasMaterial(): + material = gui_element.getMaterial() + base_color = material.getBaseColor() + current_color = QColor( + int(base_color.getX() * 255), + int(base_color.getY() * 255), + int(base_color.getZ() * 255), + int(base_color.getW() * 255) + ) + else: + # 从节点颜色获取 + node_color = gui_element.getColor() + current_color = QColor( + int(node_color.getX() * 255), + int(node_color.getY() * 255), + int(node_color.getZ() * 255), + int(node_color.getW() * 255) + ) + # current_color_obj = gui_element.getColor() + # current_color = QColor( + # int(current_color_obj[0] * 255), + # int(current_color_obj[1] * 255), + # int(current_color_obj[2] * 255) + # ) + # 对于其他类型的元素,可以添加类似的获取当前颜色的逻辑 + except: + pass # 使用默认颜色 + + color = QColorDialog.getColor(current_color, None, "选择字体颜色") + if color.isValid(): + r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 + self._updateGUITextColor(gui_element, (r, g, b, 1.0)) + + def _updateGUITextColor(self, gui_element, color): + """更新GUI元素的字体颜色""" + try: + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry"]: + # 对于DirectGUI元素,使用text_fg属性 + gui_element['text_fg'] = color + print(f"✓ 更新DirectGUI元素字体颜色: {gui_type}") + + elif gui_type == "3d_text": + # # 对于3D文本元素,直接设置颜色 + # gui_element.setColor(*color) + # print(f"✓ 更新3D文本字体颜色: {gui_type}") + + from panda3d.core import Material,Vec4 + if not gui_element.hasMaterial(): + material = Material(f"text-material-{gui_element.getName()}") + material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) + material.setDiffuse(Vec4(color[0], color[1], color[2], color[3])) + material.setAmbient(Vec4(color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, color[3])) + material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + gui_element.setMaterial(material, 1) + else: + # 更新现有材质 + material = gui_element.getMaterial() + material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) + material.setDiffuse(Vec4(color[0], color[1], color[2], color[3])) + gui_element.setMaterial(material, 1) + print(f"✓ 更新3D文本材质颜色: {color}") + + gui_element.setColor(*color) + + elif gui_type == "3d_image": + # 对于3D图片,如果有文本标签的话 + # 这里可以根据需要添加特定处理 + pass + + print(f"✓ 更新GUI元素字体颜色: {gui_type}, 颜色: {color}") + except Exception as e: + print(f"✗ 更新GUI元素字体颜色失败: {e}") + import traceback + traceback.print_exc() + + def _updateGUIScaleX(self, gui_element, scale_x): + """更新GUI元素X轴缩放""" + try: + gui_type = gui_element.getTag("gui_type") + current_scale = gui_element.getScale() + + # 对于不同的GUI类型使用不同的缩放方法 + if gui_type in ["3d_text", "3d_image"]: + # 对于3D元素,直接设置缩放 + new_scale = (scale_x, current_scale.getY(), current_scale.getZ()) + gui_element.setScale(*new_scale) + else: + # 对于2D元素,保持原有的缩放方法 + gui_element.setScale(scale_x) + + print(f"✓ 更新GUI元素X轴缩放: {scale_x}") + except Exception as e: + print(f"✗ 更新GUI元素X轴缩放失败: {e}") + + def _updateGUIScaleZ(self, gui_element, scale_z): + """更新GUI元素Y轴缩放""" + try: + gui_type = gui_element.getTag("gui_type") + current_scale = gui_element.getScale() + + # 对于不同的GUI类型使用不同的缩放方法 + if gui_type in ["3d_text", "3d_image"]: + # 对于3D元素,直接设置缩放 + new_scale = (current_scale.getX(), current_scale.getZ(), scale_z) + gui_element.setScale(*new_scale) + else: + # 对于2D元素,保持原有的缩放方法 + gui_element.setScale(scale_z) + + print(f"✓ 更新GUI元素Y轴缩放: {scale_z}") + except Exception as e: + print(f"✗ 更新GUI元素Y轴缩放失败: {e}") + + def update3DImageTexture(self,nodepath,texture_path): + try: + tex = self.world.loader.loadTexture(texture_path) + if tex: + nodepath.setTexture(tex,1) + return True + else: + print(f"[警告] 无法加载贴图: {texture_path}") + return False + except Exception as e: + print(f"[错误] 更新 3D 图片纹理失败: {e}") + return False + def _updateScriptPropertyPanel(self, game_object): """更新脚本属性面板""" # 获取对象上的脚本 -- 2.45.2