diff --git a/core/event_handler.py b/core/event_handler.py index f75b7ca4..3d5c49b9 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -389,45 +389,20 @@ class EventHandler: def _handleSelectionClick(self, hitNode): """处理选择工具的点击事件""" print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}") - - # 查找对应的实际模型节点 - selectedModel = None - - # 如果点击的是碰撞节点,找到它的父模型 - if isinstance(hitNode.node(), CollisionNode): - print(f"点击的是碰撞节点: {hitNode.getName()}") - # 碰撞节点的父节点应该是模型 - parent = hitNode.getParent() - model_list = self.world.models if hasattr(self.world, 'models') else [] - if parent in model_list or parent.hasTag('is_model_root'): - selectedModel = parent - print(f"找到对应的模型: {selectedModel.getName()}") - else: - print(f"碰撞节点的父节点不是模型: {parent.getName()}") - else: - # 查找可选择的节点(模型或其子节点) - current = hitNode - while current != self.world.render: - # 检查是否是模型 - model_list = self.world.models if hasattr(self.world, 'models') else [] - if current in model_list or current.hasTag('is_model_root'): - selectedModel = current - print(f"找到模型节点: {selectedModel.getName()}") - break - - # 检查是否是模型的子节点 - model_list = self.world.models if hasattr(self.world, 'models') else [] - for model in model_list: - if current.getParent() == model or current.isAncestorOf(model): - selectedModel = model - print(f"找到父模型: {selectedModel.getName()}") - break - if selectedModel: - break - - current = current.getParent() - + selectedModel = None + selection_system = getattr(self.world, "selection", None) + if selection_system and hasattr(selection_system, "_resolve_scene_model_owner"): + try: + selectedModel = selection_system._resolve_scene_model_owner(hitNode) + except Exception: + selectedModel = None + + if selectedModel and not selectedModel.isEmpty() and selectedModel != hitNode: + print(f"归一化选中到模型根节点: {selectedModel.getName()}") + elif isinstance(hitNode.node(), CollisionNode): + print(f"点击的是碰撞节点: {hitNode.getName()}") + if selectedModel: #print(f"✓ 最终选中模型: {selectedModel.getName()}") self.world.selection.handleMouseClick(selectedModel) diff --git a/core/selection.py b/core/selection.py index 4b6e7649..027fa0a1 100644 --- a/core/selection.py +++ b/core/selection.py @@ -124,6 +124,77 @@ class SelectionSystem: """统一获取场景树控件。""" return self._editor_context.get_tree_widget() + def _resolve_scene_model_owner(self, node): + """将场景中的任意子节点/碰撞节点归一到可编辑的模型根节点。""" + if not self._is_valid_node(node): + return node + + try: + current = node + for _ in range(64): + if not self._is_valid_node(current): + break + + runtime_owner = current.getPythonTag("animation_source_owner") + if self._is_valid_node(runtime_owner, require_attached=True): + return runtime_owner + + if current.hasTag("runtime_animation_actor") and current.getTag("runtime_animation_actor") == "true": + runtime_owner = current.getPythonTag("animation_source_owner") + if self._is_valid_node(runtime_owner, require_attached=True): + return runtime_owner + + parent = current.getParent() + if not self._is_valid_node(parent) or parent == current: + break + current = parent + except Exception: + pass + + try: + if node.hasTag("is_model_root") and node.getTag("is_model_root") == "1": + return node + except Exception: + pass + + scene_manager = getattr(self.world, "scene_manager", None) + models = getattr(scene_manager, "models", None) if scene_manager else None + if not models: + return node + + best_model = None + best_score = -1 + for model in list(models): + try: + if not self._is_valid_node(model, require_attached=True): + continue + + relation = 0 + if model == node: + relation = 4 + elif model.isAncestorOf(node): + relation = 3 + elif node.isAncestorOf(model): + relation = 2 + elif node.getParent() == model: + relation = 1 + + if relation <= 0: + continue + + score = relation * 100 + if model.hasTag("is_model_root") and model.getTag("is_model_root") == "1": + score += 20 + if model.hasTag("model_path") and model.getTag("model_path"): + score += 10 + if score > best_score: + best_score = score + best_model = model + except Exception: + continue + + return best_model or node + def _clear_tree_selection(self): """清空场景树选中项。""" tree_widget = self._get_tree_widget() @@ -736,86 +807,6 @@ class SelectionSystem: except Exception as e: print(f"设置坐标轴渲染属性失败: {str(e)}") - def updateGizmoTask(self, task): - """坐标轴更新任务 - 包含固定大小功能""" - return task.done - try: - # 限制更新频率 - if not hasattr(self, '_last_gizmo_update'): - self._last_gizmo_update = 0 - - import time - current_time = time.time() - if current_time - self._last_gizmo_update < 0.5: # 每0.05秒更新一次 - return task.cont - self._last_gizmo_update = current_time - - #检查目标节点是否已被删除 - self.checkAndClearIfTargetDeleted() - - if not self.gizmo or not self.gizmoTarget: - return task.done - - # 检查目标节点是否还存在 - if self.gizmoTarget.isEmpty(): - 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: - # 以节点位置为真值并回写 RP 灯光,避免“手柄能动但灯光不动” - self._sync_rp_light_position(self.gizmoTarget, light_object) - self.gizmo.setPos(self.gizmoTarget.getPos(self.world.render)) - else: - # 只在必要时更新位置和朝向 - self._updateGizmoPositionAndOrientation() - - # 【新功能】:动态调整坐标轴大小,保持固定的屏幕大小 - self._updateGizmoScreenSize() - - return task.cont - - except Exception as e: - print(f"坐标轴更新任务出错: {str(e)}") - return task.done def _updateGizmoPositionAndOrientation(self): """优化的Gizmo位置和朝向更新""" @@ -2074,6 +2065,7 @@ class SelectionSystem: def updateSelection(self, nodePath): try: + nodePath = self._resolve_scene_model_owner(nodePath) if self._same_valid_node(self.selectedNode, nodePath): return #print(f"\n=== 更新选择状态 ===") @@ -2089,13 +2081,6 @@ class SelectionSystem: node_name = nodePath.getName() #print(f"新选择的节点: {node_name}") - animation_tools = getattr(self.world, "animation_tools", None) - if animation_tools and hasattr(animation_tools, "_stop_all_active_animations"): - try: - animation_tools._stop_all_active_animations() - except Exception as e: - print(f"停止活动动画失败: {e}") - ssbo_editor = getattr(self.world, "ssbo_editor", None) if ssbo_editor: try: @@ -2151,51 +2136,19 @@ class SelectionSystem: if self._clear_tree_selection(): print("✓ 树形控件选中状态已清空") + try: + animation_tools = getattr(self.world, "animation_tools", None) + if animation_tools and hasattr(animation_tools, "_refresh_active_animation_visibility"): + animation_tools._refresh_active_animation_visibility() + except Exception: + pass + #print("=== 选择状态更新完成 ===\n") except Exception as e: print(f"更新选择状态失败{str(e)}") import traceback traceback.print_exc() - # def _updateSelectionVisuals(self, nodePath): - # """更新选择的视觉效果(选择框和坐标轴)""" - # try: - # if nodePath and not nodePath.isEmpty(): - # node_name = nodePath.getName() - # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") - # - # # 创建选择框 - # print("创建选择框...") - # self.createSelectionBox(nodePath) - # if self.selectionBox: - # box_name = "Unknown" - # if self.selectionBox and not self.selectionBox.isEmpty(): - # box_name = self.selectionBox.getName() - # print(f"✓ 选择框创建成功: {box_name}") - # else: - # print("× 选择框创建失败") - # - # # 创建坐标轴 - # print("创建坐标轴...") - # self.createGizmo(nodePath) - # if self.gizmo: - # gizmo_name = "Unknown" - # if self.gizmo and not self.gizmo.isEmpty(): - # gizmo_name = self.gizmo.getName() - # print(f"✓ 坐标轴创建成功: {gizmo_name}") - # else: - # print("× 坐标轴创建失败") - # - # print(f"✓ 选中了节点: {node_name}") - # else: - # print("清除选择...") - # self.clearSelectionBox() - # self.clearGizmo() - # print("✓ 取消选择") - # - # except Exception as e: - # print(f"更新选择视觉效果失败: {e}") - def getSelectedNode(self): """获取当前选中的节点""" return self._get_effective_selected_node() @@ -2250,171 +2203,6 @@ class SelectionSystem: self.selectedObject = None self._updateSelectionOutline(None) - def setupGizmoCollision(self): - return - 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): - return - # 为单个轴创建碰撞体 - 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): - return None - # 使用碰撞体检测鼠标是否悬停在坐标轴上 - 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): - return - 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()}轴节点不存在") - def focusCameraOnSelectedNodeAdvanced(self): """高级版的摄像机聚焦功能,包含平滑动画效果""" try: @@ -2835,69 +2623,6 @@ class SelectionSystem: self._double_click_task = None return task.done - # 修改 updateSelection 方法以集成双击检测 - - # def updateSelection(self, nodePath): - # """更新选择状态""" - # print(f"\n=== 更新选择状态 ===") - # - # # 如果正在删除节点,避免更新选择 - # if hasattr(self, '_deleting_node') and self._deleting_node: - # print("正在删除节点,跳过选择更新") - # print("=== 选择状态更新完成 ===\n") - # return - # - # node_name = "None" - # if nodePath and not nodePath.isEmpty(): - # node_name = nodePath.getName() - # print(f"新选择的节点: {node_name}") - # - # # 检查是否为双击 - # is_double_click = self.checkDoubleClick(nodePath) - # if is_double_click: - # print(f"检测到双击 {node_name},执行聚焦") - # # 启动聚焦(在下一帧执行,确保选择状态已更新) - # taskMgr.doMethodLater(0.01, self._delayedFocusTask, "delayedFocus") - # - # self.selectedNode = nodePath - # # 添加兼容性属性 - # self.selectedObject = nodePath - # - # if nodePath and not nodePath.isEmpty(): - # node_name = nodePath.getName() - # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") - # - # # 创建选择框 - # print("创建选择框...") - # self.createSelectionBox(nodePath) - # if self.selectionBox: - # box_name = "Unknown" - # if self.selectionBox and not self.selectionBox.isEmpty(): - # box_name = self.selectionBox.getName() - # print(f"✓ 选择框创建成功: {box_name}") - # else: - # print("× 选择框创建失败") - # - # # 创建坐标轴 - # print("创建坐标轴...") - # self.createGizmo(nodePath) - # if self.gizmo: - # gizmo_name = "Unknown" - # if self.gizmo and not self.gizmo.isEmpty(): - # gizmo_name = self.gizmo.getName() - # print(f"✓ 坐标轴创建成功: {gizmo_name}") - # else: - # print("× 坐标轴创建失败") - # - # print(f"✓ 选中了节点: {node_name}") - # else: - # print("清除选择...") - # self.clearSelectionBox() - # self.clearGizmo() - # print("✓ 取消选择") - # - # print("=== 选择状态更新完成 ===\n") - def _delayedFocusTask(self, task): """延迟执行聚焦任务""" try: diff --git a/ui/panels/animation_tools.py b/ui/panels/animation_tools.py index 586c6377..60ac5f16 100644 --- a/ui/panels/animation_tools.py +++ b/ui/panels/animation_tools.py @@ -3,7 +3,7 @@ from pathlib import Path from direct.actor.Actor import Actor from direct.task.TaskManagerGlobal import taskMgr -from panda3d.core import NodePath, PartSubset, Filename +from panda3d.core import NodePath, PartSubset, Filename, BitMask32 class _BoundAnimationProxy: @@ -121,9 +121,200 @@ class AnimationTools: def _ensure_animation_runtime_state(self): if not hasattr(self, "_animation_sync_tasks") or self._animation_sync_tasks is None: self._animation_sync_tasks = {} + if not hasattr(self, "_animation_hide_tasks") or self._animation_hide_tasks is None: + self._animation_hide_tasks = {} if not hasattr(self, "_animation_debug_enabled"): self._animation_debug_enabled = False + def _mark_runtime_animation_node(self, runtime_node, owner_model): + """Bind runtime animation nodes back to their logical owner and disable scene picking.""" + try: + if isinstance(runtime_node, _BoundAnimationProxy): + runtime_node = getattr(runtime_node, "_node", None) + if not runtime_node or runtime_node.isEmpty() or not owner_model or owner_model.isEmpty(): + return + + runtime_node.setPythonTag("animation_source_owner", owner_model) + runtime_node.setTag("runtime_animation_actor", "true") + try: + runtime_node.setCollideMask(BitMask32.allOff()) + except Exception: + pass + + try: + for child in runtime_node.findAllMatches("**"): + if not child or child.isEmpty(): + continue + child.setPythonTag("animation_source_owner", owner_model) + child.setTag("runtime_animation_actor", "true") + try: + child.setCollideMask(BitMask32.allOff()) + except Exception: + pass + except Exception: + pass + except Exception: + pass + + def _set_owner_animation_lock(self, owner_model, locked): + """Mark whether the logical owner should stay hidden while animation is active.""" + try: + if not owner_model or owner_model.isEmpty(): + return + owner_model.setPythonTag("animation_owner_locked_hidden", bool(locked)) + except Exception: + pass + + def _set_owner_stashed_for_animation(self, owner_model, stashed): + try: + if not owner_model or owner_model.isEmpty(): + return + owner_model.setPythonTag("animation_owner_stashed", bool(stashed)) + except Exception: + pass + + def _is_owner_stashed_for_animation(self, owner_model): + try: + if not owner_model or owner_model.isEmpty(): + return False + return bool(owner_model.getPythonTag("animation_owner_stashed")) + except Exception: + return False + + def _is_owner_animation_locked(self, owner_model): + try: + if not owner_model or owner_model.isEmpty(): + return False + return bool(owner_model.getPythonTag("animation_owner_locked_hidden")) + except Exception: + return False + + def _show_owner_model_if_allowed(self, owner_model, force=False): + try: + if not owner_model or owner_model.isEmpty(): + return + if (not force) and self._is_owner_animation_locked(owner_model): + self._set_owner_stashed_for_animation(owner_model, True) + owner_model.stash() + return + if self._is_owner_stashed_for_animation(owner_model): + owner_model.unstash() + self._set_owner_stashed_for_animation(owner_model, False) + owner_model.show() + except Exception: + pass + + def _refresh_active_animation_visibility(self): + """ + Re-assert visibility constraints for active animation owners. + Used after selection/UI code that may call show() on scene nodes. + """ + try: + for owner_model, actor in list(getattr(self, "_actor_cache", {}).items()): + try: + if not owner_model or owner_model.isEmpty() or not actor or actor.isEmpty(): + continue + except Exception: + continue + + try: + self._apply_actor_owner_visibility(owner_model, actor) + except Exception: + continue + except Exception: + pass + + def _can_swap_actor_owner_visibility(self, owner_model, actor): + try: + if not owner_model or owner_model.isEmpty() or not actor or actor.isEmpty(): + return False + if self._is_scene_root_node(owner_model): + return False + if actor == owner_model: + return False + except Exception: + return False + + try: + if isinstance(actor, _BoundAnimationProxy): + return getattr(actor, "_node", None) != owner_model + except Exception: + pass + return True + + def _apply_actor_owner_visibility(self, owner_model, actor, prefer_actor_visible=None): + """ + Hard invariant: + - actor visible => owner hidden + - owner visible => actor hidden + """ + try: + if not self._can_swap_actor_owner_visibility(owner_model, actor): + return + + if prefer_actor_visible is None: + try: + prefer_actor_visible = not actor.isHidden() + except Exception: + prefer_actor_visible = self._is_owner_animation_locked(owner_model) + + if prefer_actor_visible: + self._set_owner_animation_lock(owner_model, True) + self._set_owner_stashed_for_animation(owner_model, True) + owner_model.stash() + actor.show() + else: + self._set_owner_animation_lock(owner_model, False) + if self._is_owner_stashed_for_animation(owner_model): + owner_model.unstash() + self._set_owner_stashed_for_animation(owner_model, False) + actor.hide() + owner_model.show() + except Exception: + pass + + + def _ensure_owner_hidden_lock_task(self, owner_model, enabled): + self._ensure_animation_runtime_state() + task_name = self._animation_hide_tasks.get(owner_model) + + if not enabled: + if task_name: + try: + taskMgr.remove(task_name) + except Exception: + pass + self._animation_hide_tasks.pop(owner_model, None) + return + + task_name = f"maintain_anim_owner_hidden_{id(owner_model)}" + self._animation_hide_tasks[owner_model] = task_name + + def maintain_hidden(task): + try: + if not owner_model or owner_model.isEmpty(): + self._animation_hide_tasks.pop(owner_model, None) + return task.done + if not self._is_owner_animation_locked(owner_model): + self._animation_hide_tasks.pop(owner_model, None) + return task.done + actor = self._actor_cache.get(owner_model) + if actor: + self._apply_actor_owner_visibility(owner_model, actor, prefer_actor_visible=True) + else: + self._set_owner_stashed_for_animation(owner_model, True) + owner_model.stash() + return task.cont + except Exception: + self._animation_hide_tasks.pop(owner_model, None) + return task.done + + try: + taskMgr.remove(task_name) + except Exception: + pass + taskMgr.add(maintain_hidden, task_name) + def _anim_log(self, message): self._ensure_animation_runtime_state() if self._animation_debug_enabled: @@ -1061,6 +1252,7 @@ class AnimationTools: actor.reparentTo(self._get_owner_parent_node(owner_model)) actor.hide() + self._mark_runtime_animation_node(actor, owner_model) return actor except Exception as e: self._anim_log(f"[Actor加载] {source_desc} 失败: {e}") @@ -1190,6 +1382,7 @@ class AnimationTools: if proxy: proxy.reparentTo(self._get_owner_parent_node(owner_model)) proxy.hide() + self._mark_runtime_animation_node(proxy, owner_model) succeeded = True return proxy except Exception as e: @@ -1331,6 +1524,7 @@ class AnimationTools: clone_np.setName(f"{owner_model.getName()}__anim_runtime") mem_actor = _try_create_actor_from_source(clone_np, "内存模型副本") if mem_actor: + self._mark_runtime_animation_node(mem_actor, owner_model) self._actor_cache[owner_model] = mem_actor owner_model.setTag("has_animations", "true") return mem_actor @@ -1352,6 +1546,7 @@ class AnimationTools: owns_node=False ) if mem_proxy: + self._mark_runtime_animation_node(mem_proxy, owner_model) self._actor_cache[owner_model] = mem_proxy owner_model.setTag("has_animations", "true") if source_node != owner_model: @@ -1439,6 +1634,7 @@ class AnimationTools: if actor: owner_model.setTag("model_path", p) owner_model.setTag("has_animations", "true") + self._mark_runtime_animation_node(actor, owner_model) self._actor_cache[owner_model] = actor return actor @@ -1455,6 +1651,7 @@ class AnimationTools: loaded_model.hide() owner_model.setTag("model_path", p) owner_model.setTag("has_animations", "true") + self._mark_runtime_animation_node(proxy, owner_model) self._actor_cache[owner_model] = proxy return proxy loaded_model.removeNode() @@ -1489,14 +1686,36 @@ class AnimationTools: return format_name return "未知" - def _processAnimationNames(self, origin_model, anim_names): - """处理和分析动画名称,返回 [(显示名称, 原始名称), ...]""" + def _processAnimationNames(self, origin_model, anim_names, actor=None): + """处理和分析动画名称,返回 [(显示名称, 原始名称), ...]。""" format_info = self._getModelFormat(origin_model) processed = [] + seen_original_names = set() self._anim_log(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") for name in anim_names: + if not name or name in seen_original_names: + continue + + control = None + frame_count = -1 + if actor: + try: + control = actor.getAnimControl(name) + except Exception: + control = None + if control: + try: + frame_count = control.getNumFrames() + except Exception: + frame_count = -1 + + # 过滤掉无法实际播放的“伪动画名”,避免把骨骼/Bundle 名字暴露到下拉框里。 + if actor and (control is None or frame_count <= 1): + self._anim_log(f"[动画分析] 跳过无效动画名: {name} (control={bool(control)}, frames={frame_count})") + continue + display_name = name original_name = name @@ -1535,8 +1754,17 @@ class AnimationTools: display_name = name processed.append((display_name, original_name)) + seen_original_names.add(original_name) self._anim_log(f"[动画分析] {original_name} → {display_name}") + # 某些导入链路拿不到 AnimControl 时,退回原始列表,避免面板完全为空。 + if not processed and anim_names: + for name in anim_names: + if not name or name in seen_original_names: + continue + processed.append((name, name)) + seen_original_names.add(name) + return processed def _isLikelyBoneGroup(self, name): @@ -1591,12 +1819,43 @@ class AnimationTools: if not anim_names: return None - current_anim = origin_model.getPythonTag("selected_animation") - if owner_model and owner_model != origin_model and current_anim not in anim_names: - current_anim = owner_model.getPythonTag("selected_animation") + preferred_names = [] + try: + preferred_names.append(origin_model.getPythonTag("selected_animation")) + except Exception: + pass + if owner_model and owner_model != origin_model: + try: + preferred_names.append(owner_model.getPythonTag("selected_animation")) + except Exception: + pass - if current_anim not in anim_names: - current_anim = anim_names[0] + valid_names = [] + for anim_name in anim_names: + try: + control = actor.getAnimControl(anim_name) + except Exception: + control = None + if not control: + continue + try: + if control.getNumFrames() <= 1: + continue + except Exception: + pass + valid_names.append(anim_name) + + if not valid_names: + valid_names = list(anim_names) + + current_anim = None + for preferred in preferred_names: + if preferred in valid_names: + current_anim = preferred + break + + if current_anim not in valid_names: + current_anim = valid_names[0] try: origin_model.setPythonTag("selected_animation", current_anim) @@ -1658,7 +1917,7 @@ class AnimationTools: try: if self._should_swap_visibility_for_actor(cached_owner, actor): - cached_owner.show() + self._show_owner_model_if_allowed(cached_owner) except Exception: pass @@ -1721,6 +1980,16 @@ class AnimationTools: self._animation_sync_tasks.pop(owner_model, None) except Exception: pass + try: + self._ensure_owner_hidden_lock_task(owner_model, enabled=False) + except Exception: + pass + try: + if self._is_owner_stashed_for_animation(owner_model): + owner_model.unstash() + self._set_owner_stashed_for_animation(owner_model, False) + except Exception: + pass try: actor.hide() @@ -1730,7 +1999,7 @@ class AnimationTools: if restore_owner: try: if owner_model and not owner_model.isEmpty(): - owner_model.show() + self._show_owner_model_if_allowed(owner_model) except Exception: pass @@ -1791,24 +2060,40 @@ class AnimationTools: if not actor: return + # 获取当前选中的动画 + current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model) + if not current_anim: + print("『动画播放』未找到可播放的有效动画,保留原模型显示") + self._show_owner_model_if_allowed(owner_model) + return + + try: + control = actor.getAnimControl(current_anim) + except Exception: + control = None + if not control: + print(f"『动画播放』动画控制器无效: {current_anim},保留原模型显示") + self._show_owner_model_if_allowed(owner_model) + return + is_scene_bound_proxy = self._sync_actor_transform_for_playback(owner_model, actor) - if self._should_swap_visibility_for_actor(owner_model, actor): - owner_model.hide() + if self._can_swap_actor_owner_visibility(owner_model, actor): + self._ensure_owner_hidden_lock_task(owner_model, enabled=True) + self._apply_actor_owner_visibility(owner_model, actor, prefer_actor_visible=True) else: - owner_model.show() + self._set_owner_animation_lock(owner_model, False) + self._ensure_owner_hidden_lock_task(owner_model, enabled=False) + self._show_owner_model_if_allowed(owner_model) actor.show() self._ensure_actor_sync_task(owner_model, actor, enabled=(not is_scene_bound_proxy)) - # 获取当前选中的动画 - current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model) - if current_anim: - try: - actor.stop() - except Exception: - pass - actor.play(current_anim) - print(f"『动画播放』:{current_anim}") + try: + actor.stop() + except Exception: + pass + actor.play(current_anim) + print(f"『动画播放』:{current_anim}") def _pauseAnimation(self, origin_model): """暂停动画""" @@ -1839,15 +2124,15 @@ class AnimationTools: actor.setHpr(self.render, owner_model.getHpr(self.render)) actor.setScale(self.render, owner_model.getScale(self.render)) - # 隐藏原始模型,显示Actor - if self._should_swap_visibility_for_actor(owner_model, actor): - owner_model.hide() - else: - owner_model.show() - actor.show() - # 停止动画(保持当前姿势) actor.stop() + + # 显式暂停时解除隐藏锁并恢复本体显示 + self._ensure_owner_hidden_lock_task(owner_model, enabled=False) + if self._can_swap_actor_owner_visibility(owner_model, actor): + self._apply_actor_owner_visibility(owner_model, actor, prefer_actor_visible=False) + self._show_owner_model_if_allowed(owner_model, force=True) + print("『动画』暂停") def _stopAnimation(self, origin_model): @@ -1872,9 +2157,10 @@ class AnimationTools: actor.getAnimControl(current_anim).pose(0) # 隐藏Actor,显示原始模型 - if self._should_swap_visibility_for_actor(owner_model, actor): - actor.hide() - owner_model.show() + self._ensure_owner_hidden_lock_task(owner_model, enabled=False) + if self._can_swap_actor_owner_visibility(owner_model, actor): + self._apply_actor_owner_visibility(owner_model, actor, prefer_actor_visible=False) + self._show_owner_model_if_allowed(owner_model, force=True) # 移除位置维护任务 taskMgr.remove(f"maintain_anim_pos_{id(actor)}") @@ -1895,24 +2181,40 @@ class AnimationTools: if not actor: return + # 获取当前选中的动画 + current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model) + if not current_anim: + print("[动画] 未找到可循环播放的有效动画,保留原模型显示") + self._show_owner_model_if_allowed(owner_model) + return + + try: + control = actor.getAnimControl(current_anim) + except Exception: + control = None + if not control: + print(f"[动画] 动画控制器无效: {current_anim},保留原模型显示") + self._show_owner_model_if_allowed(owner_model) + return + is_scene_bound_proxy = self._sync_actor_transform_for_playback(owner_model, actor) - if self._should_swap_visibility_for_actor(owner_model, actor): - owner_model.hide() + if self._can_swap_actor_owner_visibility(owner_model, actor): + self._ensure_owner_hidden_lock_task(owner_model, enabled=True) + self._apply_actor_owner_visibility(owner_model, actor, prefer_actor_visible=True) else: - owner_model.show() + self._set_owner_animation_lock(owner_model, False) + self._ensure_owner_hidden_lock_task(owner_model, enabled=False) + self._show_owner_model_if_allowed(owner_model) actor.show() self._ensure_actor_sync_task(owner_model, actor, enabled=(not is_scene_bound_proxy)) - # 获取当前选中的动画 - current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model) - if current_anim: - try: - actor.stop() - except Exception: - pass - actor.loop(current_anim) - print(f"[动画] 循环: {current_anim}") + try: + actor.stop() + except Exception: + pass + actor.loop(current_anim) + print(f"[动画] 循环: {current_anim}") def _setAnimationSpeed(self, origin_model, speed): """设置动画播放速度""" diff --git a/ui/panels/editor_panels_right.py b/ui/panels/editor_panels_right.py index 61508831..ca46165a 100644 --- a/ui/panels/editor_panels_right.py +++ b/ui/panels/editor_panels_right.py @@ -730,7 +730,7 @@ class EditorPanelsRightMixin( # 获取和分析动画名称 anim_names = actor.getAnimNames() - processed_names = self._processAnimationNames(anim_node, anim_names) + processed_names = self._processAnimationNames(anim_node, anim_names, actor=actor) if not processed_names: imgui.text_colored((0.7, 0.7, 0.7, 1.0), "未检测到动画序列") diff --git a/ui/panels/panel_delegates.py b/ui/panels/panel_delegates.py index b60a67b9..44d2a07c 100644 --- a/ui/panels/panel_delegates.py +++ b/ui/panels/panel_delegates.py @@ -37,6 +37,12 @@ class PanelDelegates: node = selection.getSelectedNode() else: node = getattr(selection, "selectedNode", None) if selection else None + + try: + if selection and hasattr(selection, "_resolve_scene_model_owner"): + node = selection._resolve_scene_model_owner(node) + except Exception: + pass return node if self._node_is_valid(node, require_attached=False) else None def _get_ssbo_selection_summary(self):