From f81c4fd4cc1df07c853f2b3a4caf68011c9a5a07 Mon Sep 17 00:00:00 2001 From: Hector <2055590199@qq.com> Date: Mon, 18 Aug 2025 16:52:49 +0800 Subject: [PATCH] =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=98=BE=E7=A4=BA=E9=9A=90?= =?UTF-8?q?=E8=97=8F=E9=80=BB=E8=BE=91=E8=A1=A5=E5=85=A8=EF=BC=8C=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=97=A0=E7=94=A8=E9=9D=9E=E9=AA=A8=E9=AA=BC=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scene/scene_manager.py | 2 +- ui/interface_manager.py | 34 ++- ui/property_panel.py | 510 +++++----------------------------------- 3 files changed, 91 insertions(+), 455 deletions(-) diff --git a/scene/scene_manager.py b/scene/scene_manager.py index f814efec..a19eabee 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -630,7 +630,7 @@ class SceneManager: cNode.addSolid(cSphere) # 将碰撞节点附加到模型上 - cNodePath = model.attachNewNode(cNode) + #cNodePath = model.attachNewNode(cNode) # cNodePath.show() # 取消注释可以显示碰撞体,用于调试 # ==================== 场景树管理 ==================== diff --git a/ui/interface_manager.py b/ui/interface_manager.py index d45c3458..d49cf9b3 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -22,6 +22,7 @@ class InterfaceManager: # 更新场景树 self.world.scene_manager.updateSceneTree() + #self.syncVisibilityDownward() def onTreeItemClicked(self, item, column): """处理树形控件项目点击事件""" @@ -239,4 +240,35 @@ class InterfaceManager: item.setExpanded(True) for i in range(item.childCount()): - self._restore_expanded(item.child(i),path) \ No newline at end of file + self._restore_expanded(item.child(i),path) + + + def syncVisibilityDownward(self): + from collections import deque + + if not self.world.models: + return + q = deque() + + for root in self.world.models: + q.append(root) + + while q: + node = q.popleft() + + visible = node.getPythonTag("visible") + if visible is None: + visible = True + if not visible: + stack = [node] + while stack: + cur = stack.pop() + cur.hide() + + for child in cur.getChildren(): + stack.append(child) + continue + node.show() + + for child in node.getChildren(): + q.append(child) diff --git a/ui/property_panel.py b/ui/property_panel.py index 05cae777..52f87291 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 @@ -68,15 +69,34 @@ class PropertyPanelManager: self._propertyLayout.addWidget(tipLabel) return + model = item.data(0, Qt.UserRole) + + user_visible = True + if model: + user_visible = model.getPythonTag("user_visible") + if user_visible is None: + user_visible = True + model.setPythonTag("user_visible", True) + self.name_group = QGroupBox("物体名称") name_layout = QHBoxLayout() self.active_check = QCheckBox() - self.active_check.setChecked(True) # 默认激活 + # 根据模型的实际可见性状态设置复选框 + self.active_check.setChecked(user_visible) self.name_input = QLineEdit(itemText) name_layout.addWidget(self.active_check) name_layout.addWidget(self.name_input) self.name_group.setLayout(name_layout) self._propertyLayout.addWidget(self.name_group) + + if model: + try: + self.active_check.stateChanged.disconnect() + except TypeError: + pass + self.active_check.stateChanged.connect( + lambda state,m = model:self._setUserVisible(m,state == Qt.Checked) + ) # nameLabel = QLabel("名称:") # nameEdit = QLineEdit(itemText) # self._propertyLayout.addRow(nameLabel, nameEdit) @@ -107,6 +127,39 @@ class PropertyPanelManager: if propertyWidget: propertyWidget.update() + def _setUserVisible(self,node,visible): + node.setPythonTag("user_visible",visible) + self._syncEffectiveVisibility(node) + + def _syncEffectiveVisibility(self, start_node): + """广度优先,确保父隐藏则子一定隐藏""" + q = deque([(start_node, True)]) # (node, parent_effective_visible) + + while q: + node, parent_eff = q.popleft() + user = node.getPythonTag("user_visible") + if user is None: + user = True + eff = parent_eff and user + node.setPythonTag("effective_visible", eff) + + if eff: + node.show() + else: + node.hide() + + for child in node.getChildren(): + q.append((child, eff)) + + def _toggleModelVisibility(self, model, state): + """切换模型可见性状态""" + try: + # 用我们自己维护的可见性接口,而不是直接 show/hide + visible = (state == Qt.Checked) + self._setUserVisible(model, visible) + except Exception as e: + print(f"切换模型可见性失败: {str(e)}") + def refreshModelValues(self, nodePath): """实时刷新模型所有属性数值(相对/世界位置、旋转、缩放)""" if not nodePath or self._propertyLayout is None: @@ -185,7 +238,7 @@ class PropertyPanelManager: # 设置位置控件属性 for pos_widget in [self.pos_x, self.pos_y, self.pos_z]: - pos_widget.setRange(-1000, 1000) + pos_widget.setRange(-1000000.0, 1000000.0) self.pos_x.setValue(relativePos.getX()) self.pos_y.setValue(relativePos.getY()) @@ -234,7 +287,7 @@ class PropertyPanelManager: # 设置世界位置控件属性 for world_pos_widget in [self.world_pos_x, self.world_pos_y, self.world_pos_z]: - world_pos_widget.setRange(-1000, 1000) + world_pos_widget.setRange(-1000000.0, 1000000.0) world_pos_widget.setReadOnly(True) self.world_pos_x.setValue(worldPos.getX()) @@ -253,7 +306,7 @@ class PropertyPanelManager: # 设置旋转控件属性 for rot_widget in [self.rot_h, self.rot_p, self.rot_r]: - rot_widget.setRange(-180, 180) + rot_widget.setRange(-360, 360) self.rot_h.setValue(model.getH()) self.rot_p.setValue(model.getP()) @@ -4182,14 +4235,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("此模型无动画") @@ -4861,434 +4906,7 @@ except Exception as e: origin_model.setPythonTag("anim_speed",speed) print(f"[动画] 速度设为: {speed} ({display_name})") - def _detectNonSkeletalAnimations(self, origin_model): - """检测模型中的非骨骼动画""" - animations = {} - try: - print(f"[调试] 开始检测非骨骼动画: {origin_model}") - - # 1. 精确检测 AnimBundle (非骨骼动画) - bundle_nodes = origin_model.findAllMatches("**/+AnimBundleNode") - print(f"[调试] 找到 AnimBundleNode 数量: {bundle_nodes.getNumPaths()}") - - if not bundle_nodes.isEmpty(): - for i, bundle_node in enumerate(bundle_nodes): - try: - bundle = bundle_node.node().getBundle() - print(f"[调试] AnimBundle #{i}: {bundle}") - - if bundle and hasattr(bundle, 'getNumFrames'): - num_frames = bundle.getNumFrames() - print(f"[调试] Bundle 帧数: {num_frames}") - - # 只有真正有多帧的才认为是动画 - if num_frames > 1: - anim_names = [] - - # 尝试获取动画名称 - try: - if hasattr(bundle, 'getName') and bundle.getName(): - anim_names.append(bundle.getName()) - else: - anim_names.append(f"Animation_{i}") - print(f"[调试] 检测到有效帧动画: {anim_names}, {num_frames} 帧") - except Exception as name_error: - anim_names.append(f"Animation_{i}") - print(f"[调试] 使用默认动画名: Animation_{i}") - - animations['transform'] = { - 'bundle': bundle, - 'names': anim_names, - 'node': bundle_node, - 'frames': num_frames - } - else: - print(f"[调试] Bundle 只有 {num_frames} 帧,不认为是动画") - - except Exception as bundle_error: - print(f"[调试] 处理 bundle 失败: {bundle_error}") - - # 2. 仅对 GLB 文件进行特殊检测 - filepath = origin_model.getTag("model_path") - if filepath and filepath.lower().endswith('.glb') and not animations: - print(f"[调试] GLB 文件特殊检测: {filepath}") - - # 检查是否有任何动画相关的节点名称 - all_nodes = origin_model.findAllMatches("**") - anim_indicators = [] - - for node in all_nodes: - node_name = node.getName().lower() - # 查找典型的动画节点命名模式 - if any(keyword in node_name for keyword in ['anim', 'key', 'frame', 'action', 'timeline']): - anim_indicators.append(node.getName()) - print(f"[调试] 发现可能的动画节点: {node.getName()}") - - # 只有在明确找到动画指示器时才创建动画条目 - if anim_indicators: - animations['glb_keyframe'] = { - 'bundle': None, - 'names': ['GLB_Animation'], - 'node': origin_model, - 'type': 'glb_manual', - 'indicators': anim_indicators - } - print(f"[调试] 创建 GLB 动画条目,基于指示器: {anim_indicators}") - - except Exception as e: - print(f"检测非骨骼动画失败: {e}") - - print(f"[调试] 最终检测结果: {animations}") - return animations if animations else None - - def _validateNonSkeletalAnimations(self, origin_model, animations): - """验证检测到的非骨骼动画是否真的可以播放""" - try: - print(f"[验证] 开始验证非骨骼动画: {list(animations.keys())}") - - for anim_type, anim_data in animations.items(): - if anim_type == 'transform': - # 验证变换动画 - bundle = anim_data.get('bundle') - if bundle: - # 检查是否真的有可播放的动画数据 - if hasattr(bundle, 'getNumFrames'): - frames = bundle.getNumFrames() - if frames <= 1: - print(f"[验证] 变换动画帧数不足: {frames}") - return False - - # 检查是否有有效的动画通道 - try: - if hasattr(bundle, 'getNumChannels'): - channels = bundle.getNumChannels() - if channels == 0: - print(f"[验证] 无有效动画通道") - return False - except: - pass - - elif anim_type == 'glb_keyframe': - # 验证 GLB 动画指示器 - indicators = anim_data.get('indicators', []) - if not indicators: - print(f"[验证] GLB 动画无有效指示器") - return False - - print(f"[验证] 动画验证通过") - return True - - except Exception as e: - print(f"[验证] 动画验证失败: {e}") - return False - - def _buildNonSkeletalUI(self, origin_model, animations, layout): - """构建非骨骼动画控制UI""" - from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox, QTabWidget - - # 动画信息 - info_text = f"非骨骼动画数量: {len(animations)}" - info_label = QLabel(info_text) - info_label.setStyleSheet("color:#888;font-size:10px;") - layout.addWidget(QLabel("信息:"), 0, 0) - layout.addWidget(info_label, 0, 1, 1, 3) - - # 如果有多种类型的动画,使用标签页 - if len(animations) > 1: - tab_widget = QTabWidget() - - for anim_type, anim_data in animations.items(): - tab = QWidget() - tab_layout = QVBoxLayout(tab) - self._buildAnimationTypeUI(tab_layout, origin_model, anim_type, anim_data) - tab_widget.addTab(tab, self._getAnimTypeDisplayName(anim_type)) - - self._propertyLayout.addRow("动画类型:", tab_widget) - else: - # 只有一种类型,直接显示 - anim_type, anim_data = next(iter(animations.items())) - self._buildAnimationTypeUI(self._propertyLayout, origin_model, anim_type, anim_data) - - # 存储动画信息供控制使用 - if not hasattr(self, '_non_skeletal_cache'): - self._non_skeletal_cache = {} - self._non_skeletal_cache[origin_model] = animations - - def _buildAnimationTypeUI(self, layout, origin_model, anim_type, anim_data): - """为特定动画类型构建UI""" - from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox - - if anim_type == 'transform': - # 变换动画控制 - self.ns_transform_combo = QComboBox() - self.ns_transform_combo.addItems(anim_data['names']) - layout.addRow("变换动画:", self.ns_transform_combo) - - elif anim_type == 'texture': - # 纹理动画控制 - self.ns_texture_combo = QComboBox() - self.ns_texture_combo.addItems(anim_data['stages']) - layout.addRow("纹理动画:", self.ns_texture_combo) - - elif anim_type == 'material': - # 材质动画控制 - self.ns_material_combo = QComboBox() - self.ns_material_combo.addItems(anim_data['properties']) - layout.addRow("材质动画:", self.ns_material_combo) - - 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) - - # 通用控制按钮 - btn_box = QWidget() - btn_lay = QHBoxLayout(btn_box) - for txt, cmd in (("播放", "play"), ("暂停", "pause"), ("停止", "stop"), ("循环", "loop")): - btn = QPushButton(txt) - btn.clicked.connect(lambda _, c=cmd, t=anim_type: self._controlNonSkeletalAnimation(origin_model, t, c)) - btn_lay.addWidget(btn) - 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.valueChanged.connect(lambda v, t=anim_type: self._setNonSkeletalAnimationSpeed(origin_model, t, v)) - layout.addRow("播放速度:", speed_spinbox) - - def _getAnimTypeDisplayName(self, anim_type): - """获取动画类型的显示名称""" - names = { - 'transform': '变换动画', - 'glb_keyframe': 'GLB关键帧动画', - 'texture': '纹理动画', - 'material': '材质动画', - 'lerp': 'Lerp动画' - } - return names.get(anim_type, anim_type) - - def _controlNonSkeletalAnimation(self, origin_model, anim_type, command): - """控制非骨骼动画播放""" - try: - if not hasattr(self, '_non_skeletal_cache') or origin_model not in self._non_skeletal_cache: - return - - animations = self._non_skeletal_cache[origin_model] - if anim_type not in animations: - return - - anim_data = animations[anim_type] - - if anim_type == 'transform': - self._controlTransformAnimation(origin_model, anim_data, command) - elif anim_type == 'glb_keyframe': - self._controlGLBKeyframeAnimation(origin_model, anim_data, command) - elif anim_type == 'texture': - self._controlTextureAnimation(origin_model, anim_data, command) - elif anim_type == 'material': - self._controlMaterialAnimation(origin_model, anim_data, command) - elif anim_type == 'lerp': - self._controlLerpAnimation(origin_model, anim_data, command) - - print(f"[非骨骼动画] {anim_type} {command}") - - except Exception as e: - print(f"控制非骨骼动画失败: {e}") - - def _controlTransformAnimation(self, origin_model, anim_data, command): - """控制变换动画""" - try: - bundle = anim_data['bundle'] - bundle_node = anim_data['node'] - - print(f"[调试] 控制变换动画: {command}, Bundle: {bundle}") - - if command == 'play': - # 方法1: 通过 AnimBundle 直接播放 - if hasattr(bundle, 'play'): - bundle.play() - print(f"[动画] 通过 bundle.play() 播放") - # 方法2: 通过 AnimControl 播放 - elif hasattr(bundle_node.node(), 'getAnimControl'): - controls = bundle_node.node().getAnimControls() - if controls: - controls[0].play() - print(f"[动画] 通过 AnimControl 播放") - # 方法3: 通过启用动画节点 - else: - bundle_node.node().setPlayRate(1.0) - print(f"[动画] 设置播放速率为 1.0") - - elif command == 'pause': - if hasattr(bundle, 'pause'): - bundle.pause() - elif hasattr(bundle_node.node(), 'getAnimControl'): - controls = bundle_node.node().getAnimControls() - if controls: - controls[0].pause() - else: - bundle_node.node().setPlayRate(0.0) - - elif command == 'stop': - if hasattr(bundle, 'stop'): - bundle.stop() - elif hasattr(bundle_node.node(), 'getAnimControl'): - controls = bundle_node.node().getAnimControls() - if controls: - controls[0].stop() - else: - bundle_node.node().setPlayRate(0.0) - # 重置到第一帧 - if hasattr(bundle, 'setFrame'): - bundle.setFrame(0) - - elif command == 'loop': - if hasattr(bundle, 'loop'): - bundle.loop() - elif hasattr(bundle_node.node(), 'getAnimControl'): - controls = bundle_node.node().getAnimControls() - if controls: - controls[0].loop(True) - controls[0].play() - else: - bundle_node.node().setPlayRate(1.0) - - except Exception as e: - print(f"[错误] 控制变换动画失败: {e}") - import traceback - traceback.print_exc() - - def _controlGLBKeyframeAnimation(self, origin_model, anim_data, command): - """控制 GLB 关键帧动画""" - try: - print(f"[调试] 控制 GLB 动画: {command}") - - # 尝试通过 AnimControlCollection 控制 - from panda3d.core import AnimControlCollection - - # 方法1: 查找 AnimControlCollection - anim_collection = AnimControlCollection() - origin_model.getAnimControls(anim_collection) - - if anim_collection.getNumAnims() > 0: - for i in range(anim_collection.getNumAnims()): - anim_control = anim_collection.getAnim(i) - print(f"[调试] 找到动画控制: {anim_control.getName()}") - - if command == 'play': - anim_control.setPlayRate(1.0) - print(f"[GLB动画] 播放: {anim_control.getName()}") - elif command == 'pause': - anim_control.setPlayRate(0.0) - print(f"[GLB动画] 暂停: {anim_control.getName()}") - elif command == 'stop': - anim_control.setPlayRate(0.0) - anim_control.setFrame(0) - print(f"[GLB动画] 停止: {anim_control.getName()}") - elif command == 'loop': - anim_control.setPlayRate(1.0) - anim_control.loop(True) - print(f"[GLB动画] 循环: {anim_control.getName()}") - return - - # 方法2: 通过自动播放任务 - if command == 'play': - origin_model.setPlayRate(1.0) - print(f"[GLB动画] 设置播放速率为 1.0") - elif command == 'pause': - origin_model.setPlayRate(0.0) - print(f"[GLB动画] 暂停播放") - elif command == 'stop': - origin_model.setPlayRate(0.0) - print(f"[GLB动画] 停止播放") - elif command == 'loop': - origin_model.setPlayRate(1.0) - print(f"[GLB动画] 循环播放") - - except Exception as e: - print(f"[错误] 控制 GLB 动画失败: {e}") - - def _controlTextureAnimation(self, origin_model, anim_data, command): - """控制纹理动画""" - # 纹理动画通常通过 LerpInterval 实现 - print(f"[纹理动画] {command} - 功能待实现") - - def _controlMaterialAnimation(self, origin_model, anim_data, command): - """控制材质动画""" - # 材质动画通常通过修改材质属性实现 - print(f"[材质动画] {command} - 功能待实现") - - def _controlLerpAnimation(self, origin_model, anim_data, command): - """控制Lerp动画""" - # 通过 LerpInterval 控制 - print(f"[Lerp动画] {command} - 功能待实现") - - def _setNonSkeletalAnimationSpeed(self, origin_model, anim_type, speed): - """设置非骨骼动画播放速度""" - try: - if not hasattr(self, '_non_skeletal_cache') or origin_model not in self._non_skeletal_cache: - return - - animations = self._non_skeletal_cache[origin_model] - if anim_type not in animations: - return - - anim_data = animations[anim_type] - - if anim_type == 'transform': - bundle = anim_data['bundle'] - if hasattr(bundle, 'setPlayRate'): - bundle.setPlayRate(speed) - - print(f"[非骨骼动画] {anim_type} 速度设为: {speed}") - - except Exception as e: - print(f"设置非骨骼动画速度失败: {e}") - - def _detectPlayer(self,origin_model): - filepath = origin_model.getTag("model_path") - if filepath: - try: - actor = Actor(filepath) - if actor.getAnimNames(): - actor.cleanup();actor.removeNode() - return ("actor",None) - except: - pass - bundle_np = origin_model.find("**/+AnimBundleNode") - if not bundle_np.isEmpty(): - ctrl = bundle_np.node().getBundle().bind(origin_model.node(),PartGroup.PART_Whole) - return ("bundle",ctrl) - return None - - def _buildBundleUI(self,origin_model,ctrl): - from PyQt5.QtWidgets import QLabel,QPushButton,QHBoxLayout,QWidget,QSlider - - title = QLabel("骨骼动画控制") - title.setStyleSheet("color:#FF8C00;font-weight:bold;font-size:14px;margin-top:10px;") - self._propertyLayout.addRow(title) - - btn_box = QWidget() - lay = QHBoxLayout(btn_box) - for txt , fn in (("播放",ctrl.play), - ("暂停",ctrl.stop), - ("重置",lambda:ctrl.pose(0))): - btn = QPushButton(txt) - btn.clicked.connect(fn) - lay.addWidget(btn) - self._propertyLayout.addRow("控制:",btn_box) - - slider = QSlider() - slider.setOrientation(1) - slider.setRange(0,int(ctrl.getNumFrames())) - slider.valueChanged.connect(ctrl.pose) - self._propertyLayout.addRow("帧:",slider) - - self._actor_cache[origin_model] = ("bundle",ctrl) def _dispatchAnimCommand(self,origin_model,cmd): cache = self._actor_cache.get(origin_model) @@ -5323,20 +4941,6 @@ except Exception as e: elif isinstance(cmd,tuple) and cmd[0] == "speed": actor.setPlayRate(cmd[1], anim_name) - elif kind == "bundle": - ctrl = player - if cmd == "play": - ctrl.play() - elif cmd == "pause": - ctrl.stop() - elif cmd == "stop": - ctrl.stop() - ctrl.pose(0) - elif cmd == "loop": - ctrl.loop(True) - elif isinstance(cmd,tuple) and cmd[0] =="speed": - ctrl.setPlayRate(cmd[1]) - def removeActorForModel(self, model): """删除 model 对应的 Actor(如果存在)""" actor = self._actor_cache.pop(model, None)