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/3] =?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): """获取动画类型的显示名称""" 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/3] =?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): """获取动画类型的显示名称""" 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/3] =?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