import os
from pathlib import Path

from direct.actor.Actor import Actor
from direct.task.TaskManagerGlobal import taskMgr
from panda3d.core import NodePath, PartSubset


class _BoundAnimationProxy:
    """Actor-compatible wrapper backed by auto-bound AnimControls."""

    def __init__(self, node, controls, owns_node=False):
        self._node = node
        self._controls = controls
        self._owns_node = owns_node

    def isEmpty(self):
        return self._node.isEmpty()

    def show(self):
        self._node.show()

    def hide(self):
        self._node.hide()

    def setPos(self, *args):
        self._node.setPos(*args)

    def setHpr(self, *args):
        self._node.setHpr(*args)

    def setScale(self, *args):
        self._node.setScale(*args)

    def setTransform(self, *args):
        self._node.setTransform(*args)

    def getTransform(self, *args):
        return self._node.getTransform(*args)

    def reparentTo(self, *args):
        self._node.reparentTo(*args)

    def getAnimNames(self):
        return list(self._controls.keys())

    def getAnimControl(self, anim_name):
        return self._controls.get(anim_name)

    def play(self, anim_name):
        control = self.getAnimControl(anim_name)
        if control:
            try:
                control.stop()
            except Exception:
                pass
            try:
                control.pose(0)
            except Exception:
                pass
            control.play()

    def loop(self, anim_name):
        control = self.getAnimControl(anim_name)
        if control:
            # AnimControl.loop 需要 restart 参数
            try:
                control.stop()
            except Exception:
                pass
            try:
                control.pose(0)
            except Exception:
                pass
            control.loop(True)

    def stop(self):
        for control in self._controls.values():
            try:
                control.stop()
            except Exception:
                pass

    def setPlayRate(self, speed, anim_name):
        control = self.getAnimControl(anim_name)
        if control:
            control.setPlayRate(speed)

    def cleanup(self):
        if self._owns_node:
            try:
                if not self._node.isEmpty():
                    self._node.removeNode()
            except Exception:
                pass

    def removeNode(self):
        if self._owns_node:
            try:
                if not self._node.isEmpty():
                    self._node.removeNode()
            except Exception:
                pass


class AnimationTools:
    """Animation and actor helper methods extracted from main world."""

    def __init__(self, app):
        self.app = app

    def __getattr__(self, name):
        return getattr(self.app, name)

    def __setattr__(self, name, value):
        if name == "app" or name in self.__dict__ or hasattr(type(self), name):
            object.__setattr__(self, name, value)
        else:
            setattr(self.app, name, value)

    def _is_scene_root_node(self, node):
        try:
            if not node or node.isEmpty():
                return False
            name = node.getName()
            return name in ("render", "render2d", "aspect2d", "pixel2d")
        except Exception:
            return False

    def _get_owner_parent_node(self, owner_model):
        """获取动画对象的挂接父节点，确保不是空父节点。"""
        try:
            if owner_model and not owner_model.isEmpty():
                parent = owner_model.getParent()
                if parent and not parent.isEmpty():
                    return parent
        except Exception:
            pass
        return self.render

    def _node_has_geom(self, node):
        try:
            if not node or node.isEmpty():
                return False
            return node.findAllMatches("**/+GeomNode").getNumPaths() > 0
        except Exception:
            return False

    def _node_has_animation_nodes(self, node):
        try:
            if not node or node.isEmpty():
                return False
            return (
                node.findAllMatches("**/+Character").getNumPaths() > 0 or
                node.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
            )
        except Exception:
            return False

    def _prefer_owner_with_visible_geometry(self, origin_model, owner_model):
        """
        优先选择“带可见几何体”的动画 owner，避免绑定到纯骨骼节点导致播放时不可见。
        """
        try:
            if self._node_has_geom(owner_model):
                return owner_model

            candidates = []

            def _add_candidate(node, bonus=0):
                if not node or node.isEmpty() or self._is_scene_root_node(node):
                    return
                for existing, _ in candidates:
                    try:
                        if existing == node:
                            return
                    except Exception:
                        continue
                candidates.append((node, bonus))

            _add_candidate(owner_model, 100)
            _add_candidate(origin_model, 90)

            # origin/owner 祖先链中优先找可见几何体节点
            for seed, base_bonus in ((origin_model, 80), (owner_model, 70)):
                current = seed
                for depth in range(48):
                    if not current or current.isEmpty() or self._is_scene_root_node(current):
                        break
                    _add_candidate(current, base_bonus - depth)
                    parent = current.getParent()
                    if not parent or parent.isEmpty() or parent == current:
                        break
                    current = parent

            # 从 scene_manager.models 中补充候选（含弱关联兜底）
            scene_manager = getattr(self, "scene_manager", None)
            models = getattr(scene_manager, "models", None) if scene_manager else None
            if models:
                origin_name = ""
                try:
                    origin_name = (origin_model.getName() or "").strip().lower()
                except Exception:
                    origin_name = ""

                related_models = []
                weak_name_match_models = []
                anim_geom_models = []
                for model in list(models):
                    try:
                        if not model or model.isEmpty() or self._is_scene_root_node(model):
                            continue
                        related = (
                            model == origin_model or
                            model == owner_model or
                            model.isAncestorOf(origin_model) or
                            origin_model.isAncestorOf(model) or
                            model.isAncestorOf(owner_model) or
                            owner_model.isAncestorOf(model)
                        )
                        if related:
                            related_models.append(model)

                        model_name = (model.getName() or "").strip().lower()
                        if origin_name and model_name == origin_name:
                            weak_name_match_models.append(model)

                        if self._node_has_geom(model) and self._node_has_animation_nodes(model):
                            anim_geom_models.append(model)
                    except Exception:
                        continue

                for m in related_models:
                    _add_candidate(m, 75)
                for m in weak_name_match_models:
                    _add_candidate(m, 60)
                if len(anim_geom_models) == 1:
                    _add_candidate(anim_geom_models[0], 85)

            best = owner_model
            best_score = -10**9
            for node, bonus in candidates:
                try:
                    has_geom = self._node_has_geom(node)
                    has_anim = self._node_has_animation_nodes(node)
                    has_path = (
                        (node.hasTag("model_path") and bool(node.getTag("model_path"))) or
                        (node.hasTag("saved_model_path") and bool(node.getTag("saved_model_path"))) or
                        (node.hasTag("original_path") and bool(node.getTag("original_path")))
                    )
                    score = bonus
                    score += 220 if has_anim and has_geom else 0
                    score += 90 if has_geom else 0
                    score += 35 if has_anim else 0
                    score += 15 if has_path else 0
                    if score > best_score:
                        best_score = score
                        best = node
                except Exception:
                    continue

            # 把路径标签补齐到最终 owner，避免后续回退到“格式未知/纯内存”
            try:
                if best and best != owner_model:
                    for tag_name in ("model_path", "original_path", "saved_model_path", "file"):
                        if best.hasTag(tag_name) and best.getTag(tag_name):
                            continue
                        if owner_model.hasTag(tag_name) and owner_model.getTag(tag_name):
                            best.setTag(tag_name, owner_model.getTag(tag_name))
            except Exception:
                pass

            return best or owner_model
        except Exception:
            return owner_model

    def _find_scene_model_owner(self, node):
        """优先从 scene_manager.models 反查真实模型根节点。"""
        try:
            scene_manager = getattr(self, "scene_manager", None)
            models = getattr(scene_manager, "models", None)
            if not models or not node or node.isEmpty():
                return None

            best = None
            best_score = -1
            related_found = False
            for model in list(models):
                try:
                    if not model or model.isEmpty() or self._is_scene_root_node(model):
                        continue

                    # 必须与当前节点存在祖先关系
                    relation = 0
                    if model == node:
                        relation = 3
                    elif model.isAncestorOf(node):
                        relation = 2
                    elif node.isAncestorOf(model):
                        relation = 1
                    if relation == 0:
                        continue
                    related_found = True

                    has_model_path = model.hasTag("model_path") and bool(model.getTag("model_path"))
                    has_saved_path = model.hasTag("saved_model_path") and bool(model.getTag("saved_model_path"))
                    has_anim = (
                        model.findAllMatches("**/+Character").getNumPaths() > 0 or
                        model.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
                    )
                    has_geom = model.findAllMatches("**/+GeomNode").getNumPaths() > 0
                    is_model_root = model.hasTag("is_model_root") and model.getTag("is_model_root") == "1"
                    is_imported_root = model.hasTag("tree_item_type") and model.getTag("tree_item_type") == "IMPORTED_MODEL_NODE"

                    score = 0
                    score += relation * 100
                    score += 60 if has_model_path else 0
                    score += 40 if has_saved_path else 0
                    score += 30 if has_anim else 0
                    score += 20 if has_geom else 0
                    score += 10 if is_model_root else 0
                    score += 8 if is_imported_root else 0

                    if score > best_score:
                        best_score = score
                        best = model
                except Exception:
                    continue
            if best:
                return best

            # 关系反查失败时的兜底：同名优先（常见于场景重建后层级丢失）
            node_name = ""
            try:
                node_name = (node.getName() or "").strip().lower()
            except Exception:
                node_name = ""

            if not related_found and node_name:
                fallback_best = None
                fallback_score = -1
                for model in list(models):
                    try:
                        if not model or model.isEmpty() or self._is_scene_root_node(model):
                            continue
                        model_name = (model.getName() or "").strip().lower()
                        if model_name != node_name:
                            continue

                        has_model_path = model.hasTag("model_path") and bool(model.getTag("model_path"))
                        has_saved_path = model.hasTag("saved_model_path") and bool(model.getTag("saved_model_path"))
                        has_anim = (
                            model.findAllMatches("**/+Character").getNumPaths() > 0 or
                            model.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
                        )
                        has_geom = model.findAllMatches("**/+GeomNode").getNumPaths() > 0
                        score = 0
                        score += 80 if has_anim and has_geom else 0
                        score += 40 if has_model_path else 0
                        score += 25 if has_saved_path else 0
                        if score > fallback_score:
                            fallback_score = score
                            fallback_best = model
                    except Exception:
                        continue
                if fallback_best:
                    return fallback_best

            # 单模型场景兜底
            valid_models = []
            for model in list(models):
                try:
                    if model and (not model.isEmpty()) and (not self._is_scene_root_node(model)):
                        valid_models.append(model)
                except Exception:
                    continue
            if len(valid_models) == 1:
                return valid_models[0]

            # 若场景中仅有一个“动画+几何体”模型，也直接使用它
            anim_models = []
            for model in valid_models:
                try:
                    has_anim = (
                        model.findAllMatches("**/+Character").getNumPaths() > 0 or
                        model.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
                    )
                    has_geom = model.findAllMatches("**/+GeomNode").getNumPaths() > 0
                    if has_anim and has_geom:
                        anim_models.append(model)
                except Exception:
                    continue
            if len(anim_models) == 1:
                return anim_models[0]

            return None
        except Exception:
            return None

    def _recover_model_path_from_tags(self, node):
        """从常见标签恢复模型路径，尽量避免退化到纯内存 autoBind。"""
        try:
            project_root = Path(__file__).resolve().parents[2]
            search_roots = [
                project_root,
                project_root / "Resources",
                project_root / "Resources" / "models",
            ]

            raw_candidates = []
            if node and not node.isEmpty():
                for tag_name in ("model_path", "original_path", "saved_model_path", "file"):
                    try:
                        if node.hasTag(tag_name):
                            val = node.getTag(tag_name)
                            if val:
                                raw_candidates.append(val)
                    except Exception:
                        continue

            # 父链继续补齐
            walker = node.getParent() if (node and not node.isEmpty()) else None
            for _ in range(64):
                if not walker or walker.isEmpty() or self._is_scene_root_node(walker):
                    break
                for tag_name in ("model_path", "original_path", "saved_model_path", "file"):
                    try:
                        if walker.hasTag(tag_name):
                            val = walker.getTag(tag_name)
                            if val:
                                raw_candidates.append(val)
                    except Exception:
                        continue
                parent = walker.getParent()
                if not parent or parent.isEmpty() or parent == walker:
                    break
                walker = parent

            # scene_manager.models 补齐
            model_owner = self._find_scene_model_owner(node)
            if model_owner and not model_owner.isEmpty():
                for tag_name in ("model_path", "original_path", "saved_model_path", "file"):
                    try:
                        if model_owner.hasTag(tag_name):
                            val = model_owner.getTag(tag_name)
                            if val:
                                raw_candidates.append(val)
                    except Exception:
                        continue

            # 全局兜底：当当前节点标签全丢失时，从 scene_manager.models 收集唯一可用路径
            scene_manager = getattr(self, "scene_manager", None)
            models = getattr(scene_manager, "models", None) if scene_manager else None
            if models:
                scene_paths = []
                best_related_path = ""
                best_related_score = -1
                for model in list(models):
                    try:
                        if not model or model.isEmpty() or self._is_scene_root_node(model):
                            continue
                        relation = 0
                        try:
                            if model == node:
                                relation = 3
                            elif model.isAncestorOf(node):
                                relation = 2
                            elif node.isAncestorOf(model):
                                relation = 1
                        except Exception:
                            relation = 0

                        has_anim = self._node_has_animation_nodes(model)
                        has_geom = self._node_has_geom(model)

                        candidate_path = ""
                        for tag_name in ("model_path", "original_path", "saved_model_path", "file"):
                            if model.hasTag(tag_name):
                                val = model.getTag(tag_name)
                                if val:
                                    scene_paths.append(val)
                                    if (not candidate_path) and tag_name in ("model_path", "original_path", "saved_model_path"):
                                        candidate_path = val
                        if relation > 0 and candidate_path:
                            score = relation * 100
                            score += 40 if has_anim else 0
                            score += 25 if has_geom else 0
                            try:
                                if os.path.exists(candidate_path) or os.path.exists(os.path.normpath(candidate_path)):
                                    score += 35
                            except Exception:
                                pass
                            if score > best_related_score:
                                best_related_score = score
                                best_related_path = candidate_path
                    except Exception:
                        continue
                # 去重
                uniq_scene_paths = []
                seen_scene = set()
                for p in scene_paths:
                    try:
                        key = os.path.normcase(os.path.normpath(p))
                    except Exception:
                        key = p
                    if key in seen_scene:
                        continue
                    seen_scene.add(key)
                    uniq_scene_paths.append(p)
                # 仅在唯一时采用，避免误绑到别的模型
                if len(uniq_scene_paths) == 1:
                    raw_candidates.append(uniq_scene_paths[0])
                elif best_related_path:
                    raw_candidates.append(best_related_path)

            # 1) 直接存在的路径优先
            for c in raw_candidates:
                try:
                    if os.path.exists(c):
                        return c
                    norm = os.path.normpath(c)
                    if os.path.exists(norm):
                        return norm
                except Exception:
                    continue

            # 2) 基于文件名在项目目录中补查
            filenames = []
            for c in raw_candidates:
                try:
                    name = os.path.basename(c)
                    if name and "." in name:
                        filenames.append(name)
                except Exception:
                    continue

            tried = set()
            for filename in filenames:
                for root in search_roots:
                    candidate = str(root / filename)
                    key = os.path.normcase(os.path.normpath(candidate))
                    if key in tried:
                        continue
                    tried.add(key)
                    if os.path.exists(candidate):
                        return candidate

            return ""
        except Exception:
            return ""


    def _resolve_animation_owner_model(self, node):
        """向上查找动画所属的模型根节点，避免选中子节点时绑定失败。"""
        scene_owner = self._find_scene_model_owner(node)
        if scene_owner and not scene_owner.isEmpty():
            try:
                # 同步路径标签，避免后续格式识别为“未知”
                if scene_owner.hasTag("model_path") and scene_owner.getTag("model_path"):
                    if (not node.hasTag("model_path")) or (not node.getTag("model_path")):
                        node.setTag("model_path", scene_owner.getTag("model_path"))
                if scene_owner.hasTag("original_path") and scene_owner.getTag("original_path"):
                    if (not node.hasTag("original_path")) or (not node.getTag("original_path")):
                        node.setTag("original_path", scene_owner.getTag("original_path"))
            except Exception:
                pass
            return scene_owner

        try:
            current = node
            max_depth = 64
            chain = []
            for _ in range(max_depth):
                if not current or current.isEmpty():
                    break
                # 绝不把场景根节点当作动画 owner，否则 hide() 会把全场景隐藏
                if self._is_scene_root_node(current):
                    break

                chain.append(current)
                parent = current.getParent()
                if not parent or parent.isEmpty() or parent == current:
                    break
                current = parent

            # 标签缺失/层级复杂时：按可见播放优先级打分选 owner（动画+几何体最高）
            if chain:
                path_source = None
                original_path_source = None

                def score_candidate(c):
                    try:
                        has_character = c.findAllMatches("**/+Character").getNumPaths() > 0
                        has_bundle = c.findAllMatches("**/+AnimBundleNode").getNumPaths() > 0
                        has_anim = has_character or has_bundle
                        has_geom = c.findAllMatches("**/+GeomNode").getNumPaths() > 0
                    except Exception:
                        has_anim = False
                        has_geom = False

                    has_model_path = c.hasTag("model_path") and bool(c.getTag("model_path"))
                    is_model_root = c.hasTag("is_model_root") and c.getTag("is_model_root") == "1"
                    is_imported_root = c.hasTag("tree_item_type") and c.getTag("tree_item_type") == "IMPORTED_MODEL_NODE"

                    # 分数越高越优先：保证可见播放 > 仅有骨骼 > 空节点
                    score = 0
                    score += 100 if has_anim and has_geom else 0
                    score += 60 if has_model_path and has_geom else 0
                    score += 40 if has_anim else 0
                    score += 20 if has_geom else 0
                    score += 15 if has_model_path else 0
                    score += 10 if is_model_root else 0
                    score += 8 if is_imported_root else 0
                    return score

                best = chain[0]
                best_score = -1
                for candidate in chain:
                    try:
                        if not path_source and candidate.hasTag("model_path") and candidate.getTag("model_path"):
                            path_source = candidate
                        if not original_path_source and candidate.hasTag("original_path") and candidate.getTag("original_path"):
                            original_path_source = candidate
                    except Exception:
                        pass

                    s = score_candidate(candidate)
                    if s > best_score:
                        best_score = s
                        best = candidate

                # 把路径标签补到最终 owner，避免后续格式识别为“未知”
                try:
                    if path_source and ((not best.hasTag("model_path")) or (not best.getTag("model_path"))):
                        best.setTag("model_path", path_source.getTag("model_path"))
                    if original_path_source and ((not best.hasTag("original_path")) or (not best.getTag("original_path"))):
                        best.setTag("original_path", original_path_source.getTag("original_path"))
                    if best.hasTag("saved_model_path") and ((not best.hasTag("model_path")) or (not best.getTag("model_path"))):
                        best.setTag("model_path", best.getTag("saved_model_path"))
                except Exception:
                    pass

                return best
        except Exception:
            pass

        try:
            if node and not node.isEmpty() and not self._is_scene_root_node(node):
                return node
        except Exception:
            pass
        return node


    def _getActor(self, origin_model):
        """
        获取或创建模型的Actor，用于动画控制
        复用Qt版本经过验证的实现方式
        """
        owner_model = self._resolve_animation_owner_model(origin_model)
        owner_model = self._prefer_owner_with_visible_geometry(origin_model, owner_model)

        # owner 节点缺失路径标签时，尝试从当前节点及其祖先继承路径，避免只能走内存分支
        try:
            needs_path = (not owner_model.hasTag("model_path")) or (not owner_model.getTag("model_path"))
            if needs_path:
                walker = origin_model
                for _ in range(64):
                    if not walker or walker.isEmpty() or self._is_scene_root_node(walker):
                        break
                    if walker.hasTag("model_path") and walker.getTag("model_path"):
                        owner_model.setTag("model_path", walker.getTag("model_path"))
                        if walker.hasTag("original_path") and walker.getTag("original_path"):
                            owner_model.setTag("original_path", walker.getTag("original_path"))
                        break
                    if walker.hasTag("saved_model_path") and walker.getTag("saved_model_path"):
                        owner_model.setTag("model_path", walker.getTag("saved_model_path"))
                        break
                    parent = walker.getParent()
                    if not parent or parent.isEmpty() or parent == walker:
                        break
                    walker = parent

            # 仍缺路径时，尝试更激进的标签恢复
            if (not owner_model.hasTag("model_path")) or (not owner_model.getTag("model_path")):
                recovered = self._recover_model_path_from_tags(owner_model)
                if recovered:
                    owner_model.setTag("model_path", recovered)
                    if (not owner_model.hasTag("original_path")) or (not owner_model.getTag("original_path")):
                        owner_model.setTag("original_path", recovered)
        except Exception:
            pass

        def _cleanup_actor(actor):
            try:
                if actor is None:
                    return
                if hasattr(actor, "cleanup"):
                    actor.cleanup()
                if hasattr(actor, "removeNode"):
                    actor.removeNode()
            except Exception:
                pass

        def _try_create_actor_from_source(source, source_desc):
            try:
                actor = Actor(source)
                # 无论是否已检测到动画名，都显式绑定一次，避免“有名字但无可播放控制”
                try:
                    actor.bindAllAnims(allowAsyncBind=False)
                except Exception:
                    pass

                anims = actor.getAnimNames()
                print(f"[Actor加载] {source_desc} 检测到动画: {anims}")
                if not anims:
                    _cleanup_actor(actor)
                    return None

                # 确认至少有一个可用的 AnimControl
                playable = False
                for anim_name in anims:
                    try:
                        control = actor.getAnimControl(anim_name)
                        if control and control.getNumFrames() > 1:
                            playable = True
                            break
                    except Exception:
                        continue

                if not playable:
                    print(f"[Actor加载] {source_desc} 动画控制无效，尝试其他加载路径")
                    _cleanup_actor(actor)
                    return None

                # 无可见几何体的 Actor 会导致“播放后什么都看不到”
                has_geom = False
                try:
                    has_geom = actor.findAllMatches("**/+GeomNode").getNumPaths() > 0
                except Exception:
                    has_geom = False
                if not has_geom:
                    print(f"[Actor加载] {source_desc} 无可见几何体，尝试其他加载路径")
                    _cleanup_actor(actor)
                    return None

                actor.reparentTo(self._get_owner_parent_node(owner_model))
                actor.hide()
                return actor
            except Exception as e:
                print(f"[Actor加载] {source_desc} 失败: {e}")
                return None

        def _try_create_autobind_proxy(model_np, source_desc, owns_node=False):
            try:
                from panda3d.core import AnimControlCollection, autoBind

                controls = {}

                def collect_controls(np):
                    if not np or np.isEmpty():
                        return
                    try:
                        acc = AnimControlCollection()
                        autoBind(np.node(), acc, ~0)
                        for i in range(acc.getNumAnims()):
                            name = acc.getAnimName(i) or f"anim_{i}"
                            control = acc.getAnim(i)
                            if not control:
                                continue
                            if name in controls:
                                try:
                                    old_frames = controls[name].getNumFrames()
                                except Exception:
                                    old_frames = -1
                                try:
                                    new_frames = control.getNumFrames()
                                except Exception:
                                    new_frames = -1
                                # 重名时保留帧数更多的控制，避免默认选到“看起来无效”的同名动画
                                if new_frames >= old_frames:
                                    controls[name] = control
                            else:
                                controls[name] = control
                    except Exception:
                        pass

                # 先在根节点尝试，再补扫 Character 节点
                collect_controls(model_np)
                character_nodes = model_np.findAllMatches("**/+Character")
                for i in range(character_nodes.getNumPaths()):
                    collect_controls(character_nodes.getPath(i))

                # autoBind 无结果时，尝试手动把 AnimBundle 绑定到 Character 的 PartBundle
                if not controls and character_nodes.getNumPaths() > 0:
                    anim_bundle_nodes = model_np.findAllMatches("**/+AnimBundleNode")
                    subset = PartSubset()
                    for ci in range(character_nodes.getNumPaths()):
                        try:
                            character = character_nodes.getPath(ci).node()
                            for bi in range(character.getNumBundles()):
                                part_bundle = character.getBundle(bi)
                                for ai in range(anim_bundle_nodes.getNumPaths()):
                                    try:
                                        anim_bundle_node = anim_bundle_nodes.getPath(ai).node()
                                        anim_bundle = anim_bundle_node.getBundle()
                                        if not anim_bundle:
                                            continue
                                        anim_name = anim_bundle.getName() or anim_bundle_node.getName() or f"anim_{ai}"
                                        control = part_bundle.bindAnim(anim_bundle, -1, subset)
                                        if not control:
                                            continue
                                        if anim_name in controls:
                                            try:
                                                old_frames = controls[anim_name].getNumFrames()
                                            except Exception:
                                                old_frames = -1
                                            try:
                                                new_frames = control.getNumFrames()
                                            except Exception:
                                                new_frames = -1
                                            if new_frames >= old_frames:
                                                controls[anim_name] = control
                                        else:
                                            controls[anim_name] = control
                                    except Exception:
                                        continue
                        except Exception:
                            continue

                if not controls:
                    return None

                # 仅骨骼无几何体时会出现“能触发播放但场景不可见”
                try:
                    has_geom = model_np.findAllMatches("**/+GeomNode").getNumPaths() > 0
                except Exception:
                    has_geom = False
                if not has_geom:
                    print(f"[Actor加载] {source_desc} autoBind 检测到动画但无可见几何体，尝试其他节点")
                    return None

                print(f"[Actor加载] {source_desc} autoBind 检测到动画: {list(controls.keys())}")
                return _BoundAnimationProxy(model_np, controls, owns_node=owns_node)
            except Exception as e:
                print(f"[Actor加载] {source_desc} autoBind 失败: {e}")
                return None

        def _try_create_actor_via_gltf_path(path):
            """针对部分 GLB/GLTF 文件，使用 gltf 插件强制加载动画后再创建 Actor。"""
            lower_path = str(path).lower()
            if not (lower_path.endswith(".glb") or lower_path.endswith(".gltf")):
                return None

            model_np = None
            succeeded = False
            try:
                import gltf
                from panda3d.core import Filename

                panda_path = Filename.from_os_specific(path).get_fullpath()
                os_path = Filename(panda_path).to_os_specific()
                settings = gltf.GltfSettings(skip_animations=False)
                model_root = gltf.load_model(os_path, settings)
                if not model_root:
                    return None

                model_np = NodePath(model_root)
                actor = _try_create_actor_from_source(model_np, f"GLTF专用加载({path})")
                if actor:
                    succeeded = True
                    return actor

                proxy = _try_create_autobind_proxy(model_np, f"GLTF专用加载({path})", owns_node=True)
                if proxy:
                    proxy.reparentTo(self._get_owner_parent_node(owner_model))
                    proxy.hide()
                    succeeded = True
                    return proxy
            except Exception as e:
                print(f"[Actor加载] GLTF专用加载失败 ({path}): {e}")
            finally:
                # 仅在失败且本地临时节点仍存在时清理，避免泄漏
                try:
                    if (not succeeded) and model_np and not model_np.isEmpty():
                        model_np.removeNode()
                except Exception:
                    pass
            return None

        # 检查缓存（无效缓存自动清理，避免“无动画”结果被永久缓存）
        cached_actor = self._actor_cache.get(owner_model)
        if cached_actor:
            # 若已获得模型路径，优先重建真实 Actor，避免长期停留在 autoBind 代理导致“能触发但不可见”
            try:
                owner_has_path = owner_model.hasTag("model_path") and bool(owner_model.getTag("model_path"))
            except Exception:
                owner_has_path = False
            if owner_has_path and isinstance(cached_actor, _BoundAnimationProxy):
                _cleanup_actor(cached_actor)
                self._actor_cache.pop(owner_model, None)
                cached_actor = None

        if cached_actor:
            try:
                has_geom = False
                if isinstance(cached_actor, _BoundAnimationProxy):
                    has_geom = self._node_has_geom(getattr(cached_actor, "_node", None))
                else:
                    try:
                        has_geom = cached_actor.findAllMatches("**/+GeomNode").getNumPaths() > 0
                    except Exception:
                        has_geom = False

                if (not cached_actor.isEmpty()) and cached_actor.getAnimNames() and has_geom:
                    return cached_actor
            except Exception:
                pass
            _cleanup_actor(cached_actor)
            self._actor_cache.pop(owner_model, None)

        # 先检测模型树中的动画结构，并写入标签供属性面板快速判断
        try:
            character_nodes = owner_model.findAllMatches("**/+Character")
            anim_bundle_nodes = owner_model.findAllMatches("**/+AnimBundleNode")
            has_animation_nodes = (
                character_nodes.getNumPaths() > 0 or
                anim_bundle_nodes.getNumPaths() > 0
            )
            owner_model.setTag("has_animations", "true" if has_animation_nodes else "false")
            if has_animation_nodes:
                owner_model.setTag("can_create_actor_from_memory", "true")
        except Exception:
            has_animation_nodes = False

        def _try_memory_fallback():
            def _collect_autobind_source_candidates():
                candidates = []

                def add_candidate(node):
                    try:
                        if not node or node.isEmpty() or self._is_scene_root_node(node):
                            return
                        for existing in candidates:
                            if existing == node:
                                return
                        candidates.append(node)
                    except Exception:
                        return

                add_candidate(owner_model)
                add_candidate(origin_model)
                add_candidate(self._prefer_owner_with_visible_geometry(origin_model, owner_model))
                add_candidate(self._find_scene_model_owner(origin_model))
                add_candidate(self._find_scene_model_owner(owner_model))

                # 祖先链补齐
                for seed in (origin_model, owner_model):
                    current = seed
                    for _ in range(48):
                        if not current or current.isEmpty() or self._is_scene_root_node(current):
                            break
                        add_candidate(current)
                        parent = current.getParent()
                        if not parent or parent.isEmpty() or parent == current:
                            break
                        current = parent

                # scene_manager.models 补齐候选
                scene_manager = getattr(self, "scene_manager", None)
                models = getattr(scene_manager, "models", None) if scene_manager else None
                if models:
                    for model in list(models):
                        add_candidate(model)

                def score(node):
                    try:
                        has_geom = self._node_has_geom(node)
                        has_anim = self._node_has_animation_nodes(node)
                        has_path = (
                            (node.hasTag("model_path") and bool(node.getTag("model_path"))) or
                            (node.hasTag("saved_model_path") and bool(node.getTag("saved_model_path"))) or
                            (node.hasTag("original_path") and bool(node.getTag("original_path")))
                        )
                        related = False
                        try:
                            related = (
                                node == owner_model or
                                node == origin_model or
                                node.isAncestorOf(origin_model) or
                                origin_model.isAncestorOf(node) or
                                node.isAncestorOf(owner_model) or
                                owner_model.isAncestorOf(node)
                            )
                        except Exception:
                            related = False

                        s = 0
                        s += 220 if has_geom and has_anim else 0
                        s += 120 if has_geom else 0
                        s += 45 if has_anim else 0
                        s += 35 if has_path else 0
                        s += 40 if node == owner_model else 0
                        s += 30 if node == origin_model else 0
                        s += 25 if related else 0
                        return s
                    except Exception:
                        return -1

                candidates.sort(key=score, reverse=True)
                return candidates

            can_create_from_memory = False
            if owner_model.hasTag("can_create_actor_from_memory"):
                can_create_from_memory = owner_model.getTag("can_create_actor_from_memory").lower() == "true"
            if not can_create_from_memory and has_animation_nodes:
                can_create_from_memory = True

            if can_create_from_memory:
                # 不能直接 Actor(owner_model)；会污染当前场景节点，导致播放后模型消失/选择失效。
                # 先用副本创建真实 Actor，只有失败时才退回 autoBind 代理。
                clone_parent = self._get_owner_parent_node(owner_model)
                clone_np = None
                try:
                    clone_np = owner_model.copyTo(clone_parent)
                    clone_np.setName(f"{owner_model.getName()}__anim_runtime")
                    mem_actor = _try_create_actor_from_source(clone_np, "内存模型副本")
                    if mem_actor:
                        self._actor_cache[owner_model] = mem_actor
                        owner_model.setTag("has_animations", "true")
                        return mem_actor
                except Exception as e:
                    print(f"[Actor加载] 创建内存模型副本失败: {e}")
                finally:
                    # _try_create_actor_from_source 失败时，清理临时副本
                    try:
                        if clone_np and not clone_np.isEmpty() and owner_model not in self._actor_cache:
                            clone_np.removeNode()
                    except Exception:
                        pass

                # Actor 副本失败后，从多个候选节点中选择“带几何体”的 autoBind 源
                for source_node in _collect_autobind_source_candidates():
                    mem_proxy = _try_create_autobind_proxy(
                        source_node,
                        f"内存模型({source_node.getName()})",
                        owns_node=False
                    )
                    if mem_proxy:
                        self._actor_cache[owner_model] = mem_proxy
                        owner_model.setTag("has_animations", "true")
                        if source_node != owner_model:
                            print(f"[Actor加载] 使用可见节点作为动画源: {owner_model.getName()} -> {source_node.getName()}")
                        return mem_proxy
            return None

        # 始终优先尝试从文件路径加载，因为底层 gltf 插件只有在加载文件时才能抽取完整的动画名称。
        print(f"[Actor加载调试] 传入的 origin_model: {origin_model.getName() if origin_model else 'None'}")
        print(f"[Actor加载调试] origin_model tags: {origin_model.getTags() if origin_model else 'None'}")
        print(f"[Actor加载调试] 解析出的 owner_model: {owner_model.getName() if owner_model else 'None'}")
        try:
            print(f"[Actor加载调试] owner_model tags: {owner_model.getTags() if owner_model else 'None'}")
            print(f"[Actor加载调试] owner_model path tag: {owner_model.getTag('model_path') if owner_model.hasTag('model_path') else 'MISSING'}")
        except Exception as e:
            print(f"[Actor加载调试] 获取 tags 异常: {e}")

        filepath = owner_model.getTag("model_path") if owner_model.hasTag("model_path") else ""
        if not filepath and owner_model.hasTag("original_path"):
            filepath = owner_model.getTag("original_path")
        
        print(f"[Actor加载调试] 获取到的 filepath: '{filepath}'")
        if not filepath:
            print(f"[Actor加载调试] filepath为空，触发 _try_memory_fallback()")
            return _try_memory_fallback()

        # 针对Actor加载，必须使用Panda3D规范的Unix风格路径，否则Windows绝对路径会导致加载彻底崩溃并返回空节点
        panda_specific_path = ""
        try:
            from panda3d.core import Filename
            panda_specific_path = Filename.from_os_specific(filepath).get_fullpath()
        except:
            panda_specific_path = filepath.replace('\\', '/')

        candidate_paths = [panda_specific_path, filepath, os.path.normpath(filepath)]
        
        # 处理 /d/... 这类路径在 Windows 上无法直接访问的问题 (补充兜底OS路径)
        if filepath.startswith("/") and os.name == "nt":
            parts = filepath.split("/")
            if len(parts) > 2 and len(parts[1]) == 1:
                win_path = f"{parts[1].upper()}:\\{os.path.join(*parts[2:])}" if parts[2:] else f"{parts[1].upper()}:\\"
                candidate_paths.append(win_path)
                candidate_paths.append(os.path.normpath(win_path))

        # 尝试 Panda3D 路径标准化
        try:
            from scene import util
            candidate_paths.append(util.normalize_model_path(filepath))
        except Exception:
            pass

        # 在 Resources/models 中按文件名兜底查找
        filename = os.path.basename(filepath)
        if filename:
            candidate_paths.append(str(Path(__file__).resolve().parents[2] / "Resources" / "models" / filename))
            candidate_paths.append(str(Path(__file__).resolve().parents[2] / "Resources" / filename))

        # 去重并优先使用真实存在的路径 (同时确保panda专属路径排在第一位尝试)
        unique_paths = []
        seen = set()
        for p in candidate_paths:
            if not p:
                continue
            key = os.path.normcase(os.path.normpath(p))
            if key in seen:
                continue
            seen.add(key)
            unique_paths.append(p)

        # 过滤时注意如果是以 / 开头的 Panda 路径，os.path.exists 可能判断为 False，所以要额外豁免 panda_specific_path 保底加载
        existing_paths = [p for p in unique_paths if os.path.exists(p) or p == panda_specific_path]
        load_paths = existing_paths + [p for p in unique_paths if p not in existing_paths]

        for p in load_paths:
            print(f"[Actor加载验证] 正在尝试通过路径读取骨骼和动画文件: {p}")
            actor = _try_create_actor_from_source(p, f"文件路径({p})")
            if actor:
                owner_model.setTag("model_path", p)
                owner_model.setTag("has_animations", "true")
                self._actor_cache[owner_model] = actor
                return actor

            # 标准 Actor 路径失败时，针对 GLTF/GLB 走插件加载兜底
            actor = _try_create_actor_via_gltf_path(p)
            if actor:
                owner_model.setTag("model_path", p)
                owner_model.setTag("has_animations", "true")
                self._actor_cache[owner_model] = actor
                return actor

            # 路径 Actor 失败后，再尝试把文件作为普通模型加载并 autoBind
            try:
                loaded_model = self.loader.loadModel(p)
                if loaded_model and not loaded_model.isEmpty():
                    proxy = _try_create_autobind_proxy(loaded_model, f"文件路径({p})", owns_node=True)
                    if proxy:
                        loaded_model.reparentTo(self.render)
                        loaded_model.hide()
                        owner_model.setTag("model_path", p)
                        owner_model.setTag("has_animations", "true")
                        self._actor_cache[owner_model] = proxy
                        return proxy
                    loaded_model.removeNode()
            except Exception:
                pass

        # 所有创建路径失败时由内存加载进行兜底
        return _try_memory_fallback()
    

    def _getModelFormat(self, origin_model):
        """获取模型格式信息"""
        filepath = origin_model.getTag("model_path") if origin_model.hasTag("model_path") else ""
        if not filepath and origin_model.hasTag("original_path"):
            filepath = origin_model.getTag("original_path")
        if not filepath and origin_model.hasTag("saved_model_path"):
            filepath = origin_model.getTag("saved_model_path")
        if not filepath and origin_model.hasTag("file"):
            filepath = origin_model.getTag("file")
        original_path = origin_model.getTag("original_path")
        converted_from = origin_model.getTag("converted_from")

        if filepath:
            ext = filepath.lower().split('.')[-1]
            format_name = ext.upper()

            # 如果是转换后的文件，显示转换信息
            if converted_from and original_path:
                original_ext = converted_from.upper()
                format_name = f"{format_name} (从{original_ext}转换)"

            return format_name
        return "未知"

    def _processAnimationNames(self, origin_model, anim_names):
        """处理和分析动画名称，返回 [(显示名称, 原始名称), ...]"""
        format_info = self._getModelFormat(origin_model)
        processed = []

        print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}")

        for name in anim_names:
            display_name = name
            original_name = name

            format_upper = format_info.upper()

            if format_upper.startswith("GLB") or format_upper.startswith("GLTF"):
                # GLB 格式通常有真实的动画名称
                if "|" in name:
                    # 处理类似 'Armature|mixamo.com|Layer0' 的名称
                    parts = name.split("|")
                    if "mixamo" in name.lower():
                        # Mixamo 动画
                        display_name = f"Mixamo_{parts[-1]}" if len(parts) > 1 else name
                    elif len(parts) > 2:
                        # 其他复杂命名
                        display_name = f"{parts[0]}_{parts[-1]}"
                    else:
                        display_name = parts[-1]
                elif ":" in name:
                    # 一些导入器会产生前缀:动作名
                    display_name = name.split(":")[-1]
                elif "/" in name:
                    display_name = name.split("/")[-1]

            elif format_upper.startswith("FBX"):
                # FBX 格式可能需要特殊处理
                if self._isLikelyBoneGroup(name):
                    # 检查是否是骨骼组而非动画
                    print(f"[警告] '{name}' 可能不是真正的动画序列，而是骨骼组")
                    display_name = f"⚠️ {name} (可能非动画)"
                else:
                    display_name = name

            elif format_upper.startswith("EGG") or format_upper.startswith("BAM"):
                # 原生格式通常命名规范
                display_name = name

            processed.append((display_name, original_name))
            print(f"[动画分析] {original_name} → {display_name}")

        return processed

    def _isLikelyBoneGroup(self, name):
        """判断动画名称是否更像骨骼组而不是动画序列"""
        bone_indicators = ['joints', 'bones', 'skeleton', 'surface', 'mesh', 'beta', 'rig']
        name_lower = name.lower()

        # 如果包含这些关键词，可能是骨骼组
        for indicator in bone_indicators:
            if indicator in name_lower:
                return True

        # 如果名称太简单（少于3个字符），可能不是动画
        if len(name) < 3:
            return True

        return False

    def _analyzeAnimationQuality(self, actor, anim_names, format_info):
        """分析动画质量和类型（优化版本，减少详细分析以提高性能）"""
        try:
            valid_anims = 0
            
            # 简化分析：只检查动画是否存在，不详细分析帧数
            for anim_name in anim_names:
                try:
                    control = actor.getAnimControl(anim_name)
                    if control and control.getNumFrames() > 1:
                        valid_anims += 1
                except Exception:
                    # 忽略单个动画的分析错误，继续处理其他动画
                    continue

            if valid_anims == 0:
                return "⚠️ 无有效动画"
            elif valid_anims < len(anim_names):
                return f"⚠️ {valid_anims}/{len(anim_names)} 个有效"
            else:
                return f"✓ {valid_anims} 个动画"

        except Exception as e:
            # 简化错误处理
            return "分析异常"

    def _resolve_selected_animation_name(self, origin_model, actor, owner_model=None):
        """返回可播放的动画名；当当前选择无效时自动回退到首个动画并回写标签。"""
        try:
            anim_names = actor.getAnimNames() if actor else []
        except Exception:
            anim_names = []

        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")

        if current_anim not in anim_names:
            current_anim = anim_names[0]

        try:
            origin_model.setPythonTag("selected_animation", current_anim)
            if owner_model and owner_model != origin_model:
                owner_model.setPythonTag("selected_animation", current_anim)
        except Exception:
            pass

        return current_anim

    def _should_swap_visibility_for_actor(self, owner_model, actor):
        """
        是否需要“隐藏原模型/显示Actor”的切换。
        当 actor 直接绑定到 owner_model 本体时，不应切换可见性，否则会导致选中节点瞬间丢失。
        """
        if self._is_scene_root_node(owner_model):
            # 绝不隐藏场景根节点
            return False

        try:
            if isinstance(actor, _BoundAnimationProxy):
                # 非 owns_node 代理直接绑定场景节点，不能做“隐藏原模型/显示Actor”切换
                if not getattr(actor, "_owns_node", False):
                    return False
                return getattr(actor, "_node", None) != owner_model
        except Exception:
            pass

        try:
            return actor != owner_model
        except Exception:
            return True
    

    def _playAnimation(self, origin_model):
        """播放动画"""
        try:
            if hasattr(self, "render") and not self.render.isEmpty() and self.render.isHidden():
                self.render.show()
        except Exception:
            pass

        owner_model = self._resolve_animation_owner_model(origin_model)
        actor = self._getActor(owner_model)
        if not actor:
            return

        # 若仍在“无路径的内存代理”，播放前强制再尝试一次路径恢复并重建真实 Actor
        if isinstance(actor, _BoundAnimationProxy):
            owner_has_path = owner_model.hasTag("model_path") and bool(owner_model.getTag("model_path"))
            if not owner_has_path:
                recovered_path = self._recover_model_path_from_tags(owner_model) or self._recover_model_path_from_tags(origin_model)
                if recovered_path:
                    try:
                        owner_model.setTag("model_path", recovered_path)
                        if (not owner_model.hasTag("original_path")) or (not owner_model.getTag("original_path")):
                            owner_model.setTag("original_path", recovered_path)
                    except Exception:
                        pass
                    self._clear_animation_cache(owner_model)
                    actor = self._getActor(owner_model)
                    if not actor:
                        return

        is_scene_bound_proxy = isinstance(actor, _BoundAnimationProxy) and (not getattr(actor, "_owns_node", False))

        original_world_pos = owner_model.getPos(self.render)
        original_world_hpr = owner_model.getHpr(self.render)
        original_world_scale = owner_model.getScale(self.render)

        if not is_scene_bound_proxy:
            actor.setPos(owner_model.getPos())
            actor.setHpr(owner_model.getHpr())
            actor.setScale(owner_model.getScale())

        if self._should_swap_visibility_for_actor(owner_model, actor):
            owner_model.hide()
        else:
            owner_model.show()
        actor.show()

        task_name = f"maintain_anim_pos_{id(actor)}"

        # 维持 Actor 的世界变换，避免父节点层级差异导致位置抖动/漂移
        def maintainWorldPosition(task):
            try:
                if not actor.isEmpty() and not owner_model.isEmpty():
                    actor.setPos(self.render, original_world_pos)
                    actor.setHpr(self.render, original_world_hpr)
                    actor.setScale(self.render, original_world_scale)
                    return task.cont
                else:
                    return task.done
            except:
                return task.done

        taskMgr.remove(task_name)
        if not is_scene_bound_proxy:
            taskMgr.add(maintainWorldPosition, task_name)

        # 获取当前选中的动画
        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}")

    def _pauseAnimation(self, origin_model):
        """暂停动画"""
        owner_model = self._resolve_animation_owner_model(origin_model)
        actor = self._getActor(owner_model)
        if not actor:
            return

        if isinstance(actor, _BoundAnimationProxy):
            owner_has_path = owner_model.hasTag("model_path") and bool(owner_model.getTag("model_path"))
            if not owner_has_path:
                recovered_path = self._recover_model_path_from_tags(owner_model) or self._recover_model_path_from_tags(origin_model)
                if recovered_path:
                    try:
                        owner_model.setTag("model_path", recovered_path)
                        if (not owner_model.hasTag("original_path")) or (not owner_model.getTag("original_path")):
                            owner_model.setTag("original_path", recovered_path)
                    except Exception:
                        pass
                    self._clear_animation_cache(owner_model)
                    actor = self._getActor(owner_model)
                    if not actor:
                        return

        is_scene_bound_proxy = isinstance(actor, _BoundAnimationProxy) and (not getattr(actor, "_owns_node", False))
        if not is_scene_bound_proxy:
            actor.setPos(self.render, owner_model.getPos(self.render))
            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()
        print("『动画』暂停")

    def _stopAnimation(self, origin_model):
        """停止动画"""
        try:
            if hasattr(self, "render") and not self.render.isEmpty() and self.render.isHidden():
                self.render.show()
        except Exception:
            pass

        owner_model = self._resolve_animation_owner_model(origin_model)
        actor = self._getActor(owner_model)
        if not actor:
            return
            
        # 停止动画
        actor.stop()

        # 获取当前选中的动画
        current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model)
        if current_anim and actor.getAnimControl(current_anim):
            actor.getAnimControl(current_anim).pose(0)
        
        # 隐藏Actor，显示原始模型
        if self._should_swap_visibility_for_actor(owner_model, actor):
            actor.hide()
        owner_model.show()
        
        # 移除位置维护任务
        taskMgr.remove(f"maintain_anim_pos_{id(actor)}")
        
        print("『动画』停止切换至原始模型")

    def _loopAnimation(self, origin_model):
        """循环播放动画"""
        try:
            if hasattr(self, "render") and not self.render.isEmpty() and self.render.isHidden():
                self.render.show()
        except Exception:
            pass

        owner_model = self._resolve_animation_owner_model(origin_model)
        actor = self._getActor(owner_model)
        if not actor:
            return

        is_scene_bound_proxy = isinstance(actor, _BoundAnimationProxy) and (not getattr(actor, "_owns_node", False))

        original_world_pos = owner_model.getPos(self.render)
        original_world_hpr = owner_model.getHpr(self.render)
        original_world_scale = owner_model.getScale(self.render)

        if not is_scene_bound_proxy:
            actor.setPos(owner_model.getPos())
            actor.setHpr(owner_model.getHpr())
            actor.setScale(owner_model.getScale())

        if self._should_swap_visibility_for_actor(owner_model, actor):
            owner_model.hide()
        else:
            owner_model.show()
        actor.show()

        task_name = f"maintain_anim_pos_{id(actor)}"

        def maintainWorldPosition(task):
            try:
                if not actor.isEmpty() and not owner_model.isEmpty():
                    actor.setPos(self.render, original_world_pos)
                    actor.setHpr(self.render, original_world_hpr)
                    actor.setScale(self.render, original_world_scale)
                    return task.cont
                else:
                    return task.done
            except:
                return task.done

        taskMgr.remove(task_name)
        if not is_scene_bound_proxy:
            taskMgr.add(maintainWorldPosition, task_name)

        # 获取当前选中的动画
        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}")

    def _setAnimationSpeed(self, origin_model, speed):
        """设置动画播放速度"""
        owner_model = self._resolve_animation_owner_model(origin_model)
        actor = self._getActor(owner_model)
        if not actor:
            return
            
        # 获取当前选中的动画
        current_anim = self._resolve_selected_animation_name(origin_model, actor, owner_model)
        if current_anim:
            actor.setPlayRate(speed, current_anim)
            print(f"[动画] 速度设为: {speed} ({current_anim})")
        else:
            # 兜底：尝试所有动画
            anim_names = actor.getAnimNames()
            for anim_name in anim_names:
                actor.setPlayRate(speed, anim_name)
            print(f"[动画] 速度设为: {speed} (所有动画)")
    

    def _clear_animation_cache(self, node):
        """清除节点的动画缓存，当模型发生变化时调用"""
        owner_node = self._resolve_animation_owner_model(node)
        node.setPythonTag("cached_anim_info", None)
        node.setPythonTag("cached_processed_names", None)
        node.setPythonTag("animation", None)  # 同时清除动画检测结果
        
        # 如果Actor在缓存中，也需要清理
        if owner_node in self._actor_cache:
            actor = self._actor_cache[owner_node]
            try:
                # 清理相关任务
                taskMgr.remove(f"maintain_anim_pos_{id(actor)}")
                # 清理Actor
                if not actor.isEmpty():
                    actor.cleanup()
                    actor.removeNode()
            except Exception as e:
                print(f"清理Actor缓存失败: {e}")
            finally:
                del self._actor_cache[owner_node]
                print(f"[缓存清理] 清除节点 {owner_node.getName()} 的动画缓存")
