# -*- coding: utf-8 -*-
"""
大白鹅绑定工具箱 — Blender 4.5

整合多种骨骼绑定/约束/动画脚本到单个工具箱面板。
安装：Blender → 编辑 → 偏好设置 → 插件 → 从文件安装 → 选择本 .py 文件
使用：3D 视图按 N → 侧边栏「大白鹅绑定工具箱」标签，悬停按钮查看使用提示。
"""

bl_info = {
    "name": "大白鹅绑定工具箱",
    "author": "Dabaiea",
    "version": (1, 0, 0),
    "blender": (4, 5, 0),
    "location": "View3D > Sidebar (N) > 大白鹅绑定工具箱",
    "description": "骨骼绑定常用工具整合：约束、骨骼生成、绑定、动画、系统",
    "category": "Rigging",
}

import bpy
import bmesh
import mathutils
from mathutils import Vector, Quaternion


# ==================================================================
# 通用工具函数
# ==================================================================

def dabaie_flip_side_name(name):
    """把名称中的左右侧后缀互换，如 forearm.l -> forearm.r"""
    if not name or len(name) < 2:
        return None
    suffix = name[-2:]
    pairs = {
        '.l': '.r', '.L': '.R', '_l': '_r', '_L': '_R',
        '.r': '.l', '.R': '.L', '_r': '_l', '_R': '_L',
    }
    if suffix in pairs:
        return name[:-2] + pairs[suffix]
    return None


def dabaie_get_bone_chain(armature_obj, root_bone_name):
    """从根骨骼沿单子级链遍历，返回骨骼名称列表；多子级时取第一个并警告。"""
    chain = []
    current_name = root_bone_name
    bones = armature_obj.data.bones

    while current_name:
        bone = bones.get(current_name)
        if bone is None:
            break
        chain.append(current_name)

        children = list(bone.children)
        if not children:
            break
        if len(children) > 1:
            print(f"  警告: 骨骼 '{current_name}' 有 {len(children)} 个子级，"
                  f"只取第一个 '{children[0].name}'")

        current_name = children[0].name

    return chain


def dabaie_iter_action_fcurves(action):
    """遍历 action 里所有 fcurves，兼容 Blender 4.4+ 的 Slotted Actions"""
    if hasattr(action, "layers") and len(action.layers) > 0:
        for layer in action.layers:
            for strip in layer.strips:
                if hasattr(strip, "channelbags"):
                    for cb in strip.channelbags:
                        for fc in cb.fcurves:
                            yield fc
        return
    for fc in action.fcurves:
        yield fc


def dabaie_get_effective_basis(pb, arm_eval):
    """约束后、相对于父级的等效 matrix_basis（等价于 Maya 里的通道值）"""
    pb_eval = arm_eval.pose.bones[pb.name]
    if pb.parent:
        parent_eval = arm_eval.pose.bones[pb.parent.name]
        pre = (parent_eval.matrix
               @ pb.parent.bone.matrix_local.inverted()
               @ pb.bone.matrix_local)
    else:
        pre = pb.bone.matrix_local
    return pre.inverted() @ pb_eval.matrix


def dabaie_nearest_neighbor_order(points):
    """贪心最近邻排序，返回索引顺序列表。"""
    n = len(points)
    if n < 3:
        return list(range(n))
    unvisited = set(range(1, n))
    order = [0]
    while unvisited:
        cur = points[order[-1]]
        nxt = min(unvisited, key=lambda i: (points[i] - cur).length_squared)
        order.append(nxt)
        unvisited.discard(nxt)
    return order


def dabaie_deselect_all(context):
    for o in context.selected_objects:
        try:
            o.select_set(False)
        except RuntimeError:
            pass


def dabaie_back_to_mesh_edit(context, mesh_obj):
    """回到原本的网格编辑模式。"""
    dabaie_deselect_all(context)
    try:
        mesh_obj.select_set(True)
        context.view_layer.objects.active = mesh_obj
        bpy.ops.object.mode_set(mode='EDIT')
    except RuntimeError:
        pass


# ==================================================================
# 约束类型枚举
# ==================================================================

DABAIE_CONSTRAINT_TYPES = [
    ('COPY_TRANSFORMS', "复制变换", ""), ('COPY_LOCATION', "复制位置", ""),
    ('COPY_ROTATION', "复制旋转", ""), ('COPY_SCALE', "复制缩放", ""),
    ('LIMIT_LOCATION', "限制位置", ""), ('LIMIT_ROTATION', "限制旋转", ""),
    ('LIMIT_SCALE', "限制缩放", ""), ('LIMIT_DISTANCE', "限制距离", ""),
    ('MAINTAIN_VOLUME', "维持体积", ""), ('TRANSFORM', "变换", ""),
    ('TRANSFORM_CACHE', "变换缓存", ""), ('DAMPED_TRACK', "阻尼跟踪", ""),
    ('LOCKED_TRACK', "锁定跟踪", ""), ('TRACK_TO', "跟踪到", ""),
    ('STRETCH_TO', "拉伸到", ""), ('IK', "反向运动学", ""),
    ('SPLINE_IK', "样条线IK", ""), ('ARMATURE', "骨架", ""),
    ('CHILD_OF', "子级", ""), ('FLOOR', "基面", ""),
    ('FOLLOW_PATH', "跟随路径", ""), ('PIVOT', "轴心点", ""),
    ('SHRINKWRAP', "缩裹", ""), ('CLAMP_TO', "钳制到", ""),
    ('ACTION', "动作", ""), ('CAMERA_SOLVER', "摄像机解算", ""),
    ('OBJECT_SOLVER', "物体解算", ""), ('KINEMATIC', "运动学", ""),
]

DABAIE_INFLUENCE_CONSTRAINT_TYPES = [
    ('ALL', "全部约束", "修改所有约束"),
    ('COPY_LOCATION', "复制位置", "Copy Location"),
    ('COPY_ROTATION', "复制旋转", "Copy Rotation"),
    ('COPY_SCALE', "复制缩放", "Copy Scale"),
    ('COPY_TRANSFORMS', "复制变换", "Copy Transforms"),
    ('LIMIT_LOCATION', "限定位置", "Limit Location"),
    ('LIMIT_ROTATION', "限定旋转", "Limit Rotation"),
    ('LIMIT_SCALE', "限定缩放", "Limit Scale"),
    ('LIMIT_DISTANCE', "限定距离", "Limit Distance"),
    ('MAINTAIN_VOLUME', "维持体积", "Maintain Volume"),
    ('TRANSFORM', "变换", "Transform"),
    ('TRACK_TO', "跟随", "Track To"),
    ('LOCKED_TRACK', "锁定跟随", "Locked Track"),
    ('DAMPED_TRACK', "阻尼跟随", "Damped Track"),
    ('STRETCH_TO', "拉伸至", "Stretch To"),
    ('IK', "反向动力学", "Inverse Kinematics"),
    ('SPLINE_IK', "样条线IK", "Spline IK"),
    ('FOLLOW_PATH', "跟随路径", "Follow Path"),
    ('CLAMP_TO', "钳制至", "Clamp To"),
    ('CHILD_OF', "子级", "Child Of"),
    ('FLOOR', "地板", "Floor"),
    ('SHRINKWRAP', "缩裹", "Shrinkwrap"),
    ('ACTION', "动作", "Action"),
    ('ARMATURE', "骨骼", "Armature"),
    ('PYTHON', "脚本", "Script"),
    ('TRANSFORM_CACHE', "变换缓存", "Transform Cache"),
]


# ==================================================================
# 属性
# ==================================================================

def register_properties():
    bpy.types.Scene.dabaie_show_mirror = bpy.props.BoolProperty(
        name="镜像约束设置", default=False)
    bpy.types.Scene.dabaie_show_onetoone = bpy.props.BoolProperty(
        name="一对一约束设置", default=False)
    bpy.types.Scene.dabaie_show_influence = bpy.props.BoolProperty(
        name="影响值设置", default=False)
    bpy.types.Scene.dabaie_show_parent = bpy.props.BoolProperty(
        name="父级复制设置", default=False)
    bpy.types.Scene.dabaie_show_copyscale = bpy.props.BoolProperty(
        name="复制缩放设置", default=False)
    bpy.types.Scene.dabaie_show_vertex = bpy.props.BoolProperty(
        name="顶点生成设置", default=False)
    bpy.types.Scene.dabaie_show_bind = bpy.props.BoolProperty(
        name="顶点绑定设置", default=False)

    # 镜像约束
    bpy.types.Scene.dabaie_mirror_replace_target = bpy.props.BoolProperty(
        name="替换目标骨骼", default=True,
        description="自动把约束里的 subtarget (.l/.r) 换成对称骨骼")
    bpy.types.Scene.dabaie_mirror_clear_existing = bpy.props.BoolProperty(
        name="覆盖同名约束", default=True,
        description="如果镜像骨骼上已有同名约束，先删除再加")
    bpy.types.Scene.dabaie_mirror_include_muted = bpy.props.BoolProperty(
        name="包含已禁用约束", default=True,
        description="把被禁用 (mute) 的约束也一起复制")

    # 批量一对一约束
    bpy.types.Scene.dabaie_def_prefix = bpy.props.StringProperty(
        name="原骨骼前缀", default="Goal_")
    bpy.types.Scene.dabaie_ctrl_prefix = bpy.props.StringProperty(
        name="目标骨骼前缀", default="CTRL_")
    bpy.types.Scene.dabaie_constraint_type = bpy.props.EnumProperty(
        name="约束类型", items=DABAIE_CONSTRAINT_TYPES,
        default='COPY_TRANSFORMS')

    # 批量影响值
    bpy.types.Scene.dabaie_batch_constraint_type = bpy.props.EnumProperty(
        name="约束类型", items=DABAIE_INFLUENCE_CONSTRAINT_TYPES,
        default='ALL')
    bpy.types.Scene.dabaie_influence_value = bpy.props.FloatProperty(
        name="影响值", default=0.5, min=0.0, max=1.0, subtype='FACTOR',
        description="约束的影响值 (0.0 ~ 1.0)", update=dabaie_update_influence)

    # 约束旋转捕获
    bpy.types.Scene.dabaie_capture_enabled = bpy.props.BoolProperty(
        name="约束旋转捕获状态", default=False)

    # 复制骨骼为父级
    bpy.types.Scene.dabaie_bt_naming_mode = bpy.props.EnumProperty(
        name="命名方式",
        items=[
            ('ADD_PREFIX', "添加前缀", "在原骨骼名前添加文本"),
            ('ADD_SUFFIX', "添加后缀", "在原骨骼名后添加文本"),
            ('REPLACE_PREFIX', "替换前缀", "将原骨骼名的指定前缀替换为新文本"),
            ('REPLACE_SUFFIX', "替换后缀", "将原骨骼名的指定后缀替换为新文本"),
        ],
        default='ADD_SUFFIX')
    bpy.types.Scene.dabaie_bt_name_text = bpy.props.StringProperty(
        name="前缀/后缀", default="_parent",
        description="添加模式：要添加的文本；替换模式：要匹配并替换掉的旧前缀/后缀")
    bpy.types.Scene.dabaie_bt_replace_text = bpy.props.StringProperty(
        name="替换为", default="",
        description="替换模式下使用的新前缀/后缀")
    bpy.types.Scene.dabaie_bt_manage_collection = bpy.props.BoolProperty(
        name="管理骨骼集合", default=True,
        description="把新建的父级骨骼放入指定的骨骼集合中")
    bpy.types.Scene.dabaie_bt_collection_name = bpy.props.StringProperty(
        name="集合名称", default="ParentBones",
        description="目标骨骼集合的名称。若不存在则自动创建")

    # 复制并缩放骨骼
    bpy.types.Scene.dabaie_orig_prefix = bpy.props.StringProperty(
        name="原前缀", default="Goal_",
        description="原骨骼的名字前缀，例如 Goal_")
    bpy.types.Scene.dabaie_new_prefix = bpy.props.StringProperty(
        name="新前缀", default="CTRL_",
        description="新骨骼的名字前缀，例如 CTRL_")
    bpy.types.Scene.dabaie_scale_ratio = bpy.props.FloatProperty(
        name="厚度缩放比例", default=0.5, min=0.01, max=10.0,
        description="新骨骼柔性显示尺寸相对于原骨骼的比例，长度自动按反比延长")

    # 顶点生成骨骼
    bpy.types.Scene.dabaie_vtb_target = bpy.props.PointerProperty(
        name="目标骨架", type=bpy.types.Object,
        description="骨骼将被添加到这个骨架中；留空则自动新建一个",
        poll=lambda self, obj: obj.type == 'ARMATURE')
    bpy.types.Scene.dabaie_vtb_mode = bpy.props.EnumProperty(
        name="生成模式",
        items=[
            ('CHAIN', "链条", "把选中的点依次连成一串骨骼"),
            ('EACH', "独立骨骼", "每个选中点生成一根骨骼"),
        ],
        default='CHAIN')
    bpy.types.Scene.dabaie_vtb_sort = bpy.props.EnumProperty(
        name="排序方式",
        items=[
            ('INDEX', "按顶点索引", "按网格顶点索引顺序排列"),
            ('NEAREST', "最近邻", "按空间距离自动排序（适合连成一串）"),
        ],
        default='NEAREST')
    bpy.types.Scene.dabaie_vtb_connect = bpy.props.BoolProperty(
        name="骨骼相连", default=True,
        description="链条模式下让相邻骨骼首尾相连")
    bpy.types.Scene.dabaie_vtb_axis = bpy.props.EnumProperty(
        name="方向",
        items=[
            ('NEXT', "指向下一个点", "骨尾指向相邻的下一个点"),
            ('Z', "沿 Z 轴", "骨尾沿世界 Z 轴延伸"),
        ],
        default='NEXT')
    bpy.types.Scene.dabaie_vtb_length_factor = bpy.props.FloatProperty(
        name="长度系数", default=0.5, min=0.01, max=10.0,
        description="独立骨骼模式下：骨骼长度 = 参考距离 × 该系数")
    bpy.types.Scene.dabaie_vtb_prefix = bpy.props.StringProperty(
        name="骨骼名前缀", default="Bone_")

    # 骨骼顶点智能绑定
    bpy.types.Scene.dabaie_proxy_mesh = bpy.props.PointerProperty(
        name="代理模型", type=bpy.types.Object,
        description="选择用于提供顶点位置的网格模型")
    bpy.types.Scene.dabaie_create_all_vgroups = bpy.props.BoolProperty(
        name="为所有顶点创建顶点组", default=True,
        description="顶点很多时建议关闭，只在实际匹配到时创建以节省性能")


def unregister_properties():
    for name in [
        "dabaie_show_mirror", "dabaie_show_onetoone", "dabaie_show_influence",
        "dabaie_show_parent", "dabaie_show_copyscale", "dabaie_show_vertex",
        "dabaie_show_bind",
        "dabaie_mirror_replace_target", "dabaie_mirror_clear_existing",
        "dabaie_mirror_include_muted",
        "dabaie_def_prefix", "dabaie_ctrl_prefix", "dabaie_constraint_type",
        "dabaie_batch_constraint_type", "dabaie_influence_value",
        "dabaie_capture_enabled",
        "dabaie_bt_naming_mode", "dabaie_bt_name_text", "dabaie_bt_replace_text",
        "dabaie_bt_manage_collection", "dabaie_bt_collection_name",
        "dabaie_orig_prefix", "dabaie_new_prefix", "dabaie_scale_ratio",
        "dabaie_vtb_target", "dabaie_vtb_mode", "dabaie_vtb_sort",
        "dabaie_vtb_connect", "dabaie_vtb_axis", "dabaie_vtb_length_factor",
        "dabaie_vtb_prefix",
        "dabaie_proxy_mesh", "dabaie_create_all_vgroups",
    ]:
        if hasattr(bpy.types.Scene, name):
            delattr(bpy.types.Scene, name)


# ==================================================================
# 属性回调
# ==================================================================

def dabaie_update_influence(self, context):
    """滑块拖动时实时应用到选中骨骼的匹配约束"""
    if context.mode == 'POSE' and context.object and context.object.type == 'ARMATURE':
        armature = context.object
        target_type = self.dabaie_batch_constraint_type
        val = self.dabaie_influence_value
        for bone in armature.pose.bones:
            if not bone.bone.select:
                continue
            for c in bone.constraints:
                if target_type != 'ALL' and c.type != target_type:
                    continue
                if hasattr(c, 'influence'):
                    c.influence = val


# ==================================================================
# 约束旋转捕获处理器
# ==================================================================

def dabaie_capture_constraint_rot(scene, depsgraph):
    for arm in bpy.data.objects:
        if arm.type != 'ARMATURE':
            continue
        arm_eval = arm.evaluated_get(depsgraph)
        for pb in arm.pose.bones:
            if not pb.constraints:
                continue
            q = dabaie_get_effective_basis(pb, arm_eval).to_quaternion()
            pb["eff_quat"] = (q.w, q.x, q.y, q.z)
            if q.angle > 1e-5:
                q15 = Quaternion(q.axis, q.angle * 1.5)
            else:
                q15 = Quaternion()
            pb["eff_quat_x15"] = (q15.w, q15.x, q15.y, q15.z)


def dabaie_capture_is_enabled():
    return any(h.__name__ == "dabaie_capture_constraint_rot"
               for h in bpy.app.handlers.depsgraph_update_post)


# ==================================================================
# 操作符：镜像约束
# ==================================================================

class DABAIE_OT_mirror_constraints(bpy.types.Operator):
    bl_idname = "dabaie.mirror_constraints"
    bl_label = "镜像约束"
    bl_description = ("在姿态模式选中一侧骨骼，把它们的约束镜像复制到对称骨骼"
                      "（.l↔.r / _l↔_r），并自动替换目标骨骼为对称骨骼")
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        armature = context.object
        if not armature or armature.type != 'ARMATURE':
            self.report({'ERROR'}, "请选择一个骨架对象")
            return {'CANCELLED'}
        if context.mode != 'POSE':
            self.report({'ERROR'}, "请在姿态模式下使用")
            return {'CANCELLED'}

        source_bones = [b for b in armature.pose.bones if b.bone.select]
        if not source_bones:
            self.report({'WARNING'}, "没有选中骨骼")
            return {'CANCELLED'}

        scene = context.scene
        bone_count = 0
        constraint_count = 0
        no_mirror = []

        for bone in source_bones:
            mirror_name = dabaie_flip_side_name(bone.name)
            if not mirror_name:
                continue
            mirror_bone = armature.pose.bones.get(mirror_name)
            if not mirror_bone:
                no_mirror.append(bone.name)
                continue
            if not bone.constraints:
                continue

            bone_count += 1
            for src_c in bone.constraints:
                if not scene.dabaie_mirror_include_muted and src_c.mute:
                    continue
                if scene.dabaie_mirror_clear_existing:
                    existing = mirror_bone.constraints.get(src_c.name)
                    if existing:
                        mirror_bone.constraints.remove(existing)
                if self._copy_constraint(src_c, mirror_bone, armature, scene):
                    constraint_count += 1

        msg = f"已为 {bone_count} 根骨骼镜像 {constraint_count} 个约束"
        if no_mirror:
            msg += f" | {len(no_mirror)} 根未找到对称骨骼"
        self.report({'INFO'}, msg)
        return {'FINISHED'}

    def _copy_constraint(self, src, dst_bone, armature, scene):
        try:
            new_c = dst_bone.constraints.new(type=src.type)
        except Exception as e:
            print(f"[大白鹅工具箱] 无法创建约束: {e}")
            return None

        for prop in src.bl_rna.properties:
            pid = prop.identifier
            if pid in {'rna_type', 'type', 'name'}:
                continue
            if prop.is_readonly:
                continue
            try:
                value = getattr(src, pid)
                if prop.type == 'POINTER':
                    setattr(new_c, pid, value)
                elif prop.type == 'STRING':
                    if scene.dabaie_mirror_replace_target and value:
                        flipped = dabaie_flip_side_name(value)
                        if flipped and armature.pose.bones.get(flipped):
                            setattr(new_c, pid, flipped)
                            continue
                    setattr(new_c, pid, value)
                elif prop.type in {'FLOAT', 'INT', 'BOOLEAN', 'ENUM'}:
                    setattr(new_c, pid, value)
            except Exception:
                pass

        try:
            new_c.name = src.name
        except Exception:
            pass
        return new_c


# ==================================================================
# 操作符：批量一对一约束
# ==================================================================

class DABAIE_OT_constrain_def_by_ctrl(bpy.types.Operator):
    bl_idname = "dabaie.constrain_def_by_ctrl"
    bl_label = "批量一对一约束"
    bl_description = ("在姿态模式选中需要被约束的骨骼，按其名称前缀替换成目标前缀，"
                      "为每根骨骼一对一添加指定类型约束（自动去重）")
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        scene = context.scene
        armature_obj = context.object
        if not armature_obj or armature_obj.type != 'ARMATURE':
            self.report({'ERROR'}, "请先激活一个骨架物体")
            return {'CANCELLED'}
        if armature_obj.mode != 'POSE':
            self.report({'ERROR'}, "请进入【姿态模式】后再执行此操作")
            return {'CANCELLED'}

        selected_pose_bones = context.selected_pose_bones
        if not selected_pose_bones:
            self.report({'ERROR'}, "请至少选中一根需要被约束的骨骼")
            return {'CANCELLED'}

        def_prefix = scene.dabaie_def_prefix
        ctrl_prefix = scene.dabaie_ctrl_prefix
        constraint_type = scene.dabaie_constraint_type

        mismatched_bones = []
        for pb in selected_pose_bones:
            if not pb.name.startswith(def_prefix):
                mismatched_bones.append(pb.name)
        if mismatched_bones:
            error_msg = (f"以下骨骼不以 '{def_prefix}' 开头，操作已取消: "
                         + ", ".join(mismatched_bones[:5]))
            if len(mismatched_bones) > 5:
                error_msg += " ... (等)"
            self.report({'ERROR'}, error_msg)
            return {'CANCELLED'}

        all_pose_bones = armature_obj.pose.bones
        success_count = 0
        missing_bones = []

        for pb in selected_pose_bones:
            base_name = pb.name[len(def_prefix):]
            ctrl_name = f"{ctrl_prefix}{base_name}"
            if ctrl_name not in all_pose_bones:
                missing_bones.append(ctrl_name)
                continue

            for c in list(pb.constraints):
                if c.name.startswith("Auto_"):
                    pb.constraints.remove(c)

            constraint = pb.constraints.new(constraint_type)
            constraint.name = f"Auto_{constraint_type}"

            if constraint_type == 'ARMATURE':
                target_item = constraint.targets.new()
                target_item.target = armature_obj
                target_item.subtarget = ctrl_name
            else:
                constraint.target = armature_obj
                constraint.subtarget = ctrl_name
                if constraint_type == 'IK':
                    constraint.chain_count = 2

            success_count += 1

        if missing_bones:
            self.report({'WARNING'},
                        f"成功约束 {success_count} 根骨骼。但未找到: {', '.join(missing_bones)}")
        else:
            self.report({'INFO'},
                        f"成功：为 {success_count} 根骨骼添加了 '{constraint_type}' 约束。")
        return {'FINISHED'}


# ==================================================================
# 操作符：批量修改影响值
# ==================================================================

class DABAIE_OT_batch_apply_influence(bpy.types.Operator):
    bl_idname = "dabaie.batch_apply_influence"
    bl_label = "应用影响值"
    bl_description = "把当前影响值应用到选中骨骼上指定类型的约束"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        scene = context.scene
        armature = context.object
        if not armature or armature.type != 'ARMATURE':
            self.report({'ERROR'}, "请选择一个骨架对象")
            return {'CANCELLED'}
        if context.mode != 'POSE':
            self.report({'ERROR'}, "请在姿态模式下使用")
            return {'CANCELLED'}

        selected_bones = [b for b in armature.pose.bones if b.bone.select]
        if not selected_bones:
            self.report({'WARNING'}, "没有选中任何骨骼")
            return {'CANCELLED'}

        target_type = scene.dabaie_batch_constraint_type
        influence = scene.dabaie_influence_value

        count = 0
        for bone in selected_bones:
            for c in bone.constraints:
                if target_type != 'ALL' and c.type != target_type:
                    continue
                if hasattr(c, 'influence'):
                    c.influence = influence
                    count += 1

        self.report({'INFO'}, f"已修改 {count} 个约束的影响值 → {influence:.2f}")
        return {'FINISHED'}


class DABAIE_OT_batch_set_influence(bpy.types.Operator):
    bl_idname = "dabaie.batch_set_influence"
    bl_label = "设置影响值"
    bl_description = "快速设置影响值并应用到选中骨骼"
    bl_options = {'REGISTER', 'UNDO'}

    value: bpy.props.FloatProperty(default=0.0)

    def execute(self, context):
        context.scene.dabaie_influence_value = self.value
        bpy.ops.dabaie.batch_apply_influence()
        return {'FINISHED'}


class DABAIE_OT_batch_read_influence(bpy.types.Operator):
    bl_idname = "dabaie.batch_read_influence"
    bl_label = "读取影响值"
    bl_description = "从选中骨骼的第一个匹配约束读取影响值"
    bl_options = {'REGISTER'}

    def execute(self, context):
        scene = context.scene
        armature = context.object
        if not armature or armature.type != 'ARMATURE':
            return {'CANCELLED'}
        if context.mode != 'POSE':
            return {'CANCELLED'}

        target_type = scene.dabaie_batch_constraint_type
        for bone in armature.pose.bones:
            if not bone.bone.select:
                continue
            for c in bone.constraints:
                if target_type != 'ALL' and c.type != target_type:
                    continue
                if hasattr(c, 'influence'):
                    scene.dabaie_influence_value = c.influence
                    self.report({'INFO'},
                                f"读取影响值: {c.influence:.2f} (来自 {bone.name} / {c.name})")
                    return {'FINISHED'}
        self.report({'WARNING'}, "未找到匹配的约束")
        return {'CANCELLED'}


# ==================================================================
# 操作符：关节链约束
# ==================================================================

class DABAIE_OT_joint_chain(bpy.types.Operator):
    bl_idname = "dabaie.joint_chain"
    bl_label = "关节链约束"
    bl_description = ("在姿态模式选中 ≥2 根骨骼根，最后点击的作为权重链根，其余作为控制器链根，"
                      "沿子级链为权重链逐根添加 COPY_TRANSFORMS（用于 IK/FK 切换）")
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        armature_obj = context.object
        if armature_obj is None or armature_obj.type != 'ARMATURE':
            self.report({'ERROR'}, "请选中一个骨架对象")
            return {'CANCELLED'}
        if context.mode != 'POSE':
            self.report({'ERROR'}, "请在姿态模式下使用")
            return {'CANCELLED'}

        selected_names = [b.name for b in armature_obj.data.bones if b.select]
        if len(selected_names) < 2:
            self.report({'ERROR'}, "请至少选中 2 根骨骼根（控制器链根 + 权重链根）")
            return {'CANCELLED'}

        active_bone = context.active_pose_bone
        if active_bone is None or active_bone.name not in selected_names:
            self.report({'ERROR'}, "请让权重链根骨骼成为活动骨骼（最后点击的那根）")
            return {'CANCELLED'}

        target_root = active_bone.name
        source_roots = [n for n in selected_names if n != target_root]

        target_chain = dabaie_get_bone_chain(armature_obj, target_root)
        print(f"\n权重链 Z (根: {target_root})，共 {len(target_chain)} 根骨骼:")
        for i, name in enumerate(target_chain):
            print(f"  [{i}] {name}")

        added_total = 0
        skipped_chains = 0
        for source_root in source_roots:
            source_chain = dabaie_get_bone_chain(armature_obj, source_root)
            if len(source_chain) != len(target_chain):
                print(f"跳过链 '{source_root}': 长度不匹配 "
                      f"(源:{len(source_chain)}, 目标:{len(target_chain)})")
                skipped_chains += 1
                continue

            prefix = f"CopyFrom_{source_root}_"
            for i, source_bone_name in enumerate(source_chain):
                target_bone_name = target_chain[i]
                target_pose_bone = armature_obj.pose.bones[target_bone_name]

                already_exists = any(
                    c.type == 'COPY_TRANSFORMS' and c.target == armature_obj
                    and c.subtarget == source_bone_name
                    for c in target_pose_bone.constraints)
                if already_exists:
                    continue

                constraint = target_pose_bone.constraints.new('COPY_TRANSFORMS')
                constraint.name = f"{prefix}{source_bone_name}"
                constraint.target = armature_obj
                constraint.subtarget = source_bone_name
                added_total += 1

        self.report({'INFO'},
                    f"完成：添加 {added_total} 个约束，跳过 {skipped_chains} 条长度不匹配的链")
        return {'FINISHED'}


# ==================================================================
# 操作符：清除选中骨骼约束
# ==================================================================

class DABAIE_OT_clean_constraints(bpy.types.Operator):
    bl_idname = "dabaie.clean_constraints"
    bl_label = "清除选中骨骼约束"
    bl_description = "删除当前选中骨骼上的全部约束（姿态模式，选中骨骼后点击）"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        obj = context.object
        if obj is None or obj.type != 'ARMATURE':
            self.report({'ERROR'}, "请先选中一个骨架物体")
            return {'CANCELLED'}
        if context.mode != 'POSE':
            self.report({'ERROR'}, "请在姿态模式下使用")
            return {'CANCELLED'}

        count = 0
        for pb in obj.pose.bones:
            if pb.bone.select:
                for con in list(pb.constraints):
                    pb.constraints.remove(con)
                    count += 1

        self.report({'INFO'}, f"已删除选中骨骼上的 {count} 个约束")
        return {'FINISHED'}


# ==================================================================
# 操作符：约束旋转捕获（开关）
# ==================================================================

class DABAIE_OT_toggle_capture_rotation(bpy.types.Operator):
    bl_idname = "dabaie.toggle_capture_rotation"
    bl_label = "约束旋转捕获"
    bl_description = ("开关：注册/移除 depsgraph 处理器，持续把约束后的等效旋转写入骨骼自定义属性 "
                      "eff_quat 与 eff_quat_x15（1.5 倍轴角）")
    bl_options = {'REGISTER'}

    def execute(self, context):
        if dabaie_capture_is_enabled():
            for h in list(bpy.app.handlers.depsgraph_update_post):
                if h.__name__ == "dabaie_capture_constraint_rot":
                    bpy.app.handlers.depsgraph_update_post.remove(h)
            context.scene.dabaie_capture_enabled = False
            self.report({'INFO'}, "已停用约束旋转捕获")
        else:
            bpy.app.handlers.depsgraph_update_post.append(dabaie_capture_constraint_rot)
            context.scene.dabaie_capture_enabled = True
            self.report({'INFO'}, "已启用约束旋转捕获（记录 eff_quat / eff_quat_x15）")
        return {'FINISHED'}


# ==================================================================
# 操作符：复制骨骼为父级
# ==================================================================

def dabaie_compute_name(original_name, props):
    mode = props.dabaie_bt_naming_mode
    text = props.dabaie_bt_name_text
    replace_text = props.dabaie_bt_replace_text

    if mode == 'ADD_PREFIX':
        return text + original_name
    if mode == 'ADD_SUFFIX':
        return original_name + text
    if mode == 'REPLACE_PREFIX':
        if text and original_name.startswith(text):
            return replace_text + original_name[len(text):]
        return original_name
    if mode == 'REPLACE_SUFFIX':
        if text and original_name.endswith(text):
            return original_name[:-len(text)] + replace_text
        return original_name
    return original_name


class DABAIE_OT_duplicate_as_parent(bpy.types.Operator):
    bl_idname = "dabaie.duplicate_as_parent"
    bl_label = "复制骨骼为父级"
    bl_description = ("在编辑/姿态模式选中骨骼，为每根骨骼创建一份复制，"
                      "并把原骨骼设为新复制骨骼的子级，可加入指定骨骼集合")
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        obj = context.object
        return obj is not None and obj.type == 'ARMATURE' and obj.mode in {'EDIT', 'POSE'}

    def execute(self, context):
        scene = context.scene
        arm_obj = context.object
        original_mode = arm_obj.mode

        if original_mode != 'EDIT':
            try:
                bpy.ops.object.mode_set(mode='EDIT')
            except RuntimeError:
                self.report({'ERROR'}, "无法进入骨骼编辑模式")
                return {'CANCELLED'}

        edit_bones = arm_obj.data.edit_bones

        selected_data = []
        for eb in edit_bones:
            if eb.select:
                selected_data.append({
                    'name': eb.name,
                    'head': eb.head.copy(),
                    'tail': eb.tail.copy(),
                    'roll': eb.roll,
                    'parent_name': eb.parent.name if eb.parent else None,
                })

        if not selected_data:
            self.report({'WARNING'}, "没有选中任何骨骼")
            if original_mode != 'EDIT':
                bpy.ops.object.mode_set(mode=original_mode)
            return {'CANCELLED'}

        existing_names = {eb.name for eb in edit_bones}
        name_map = {}

        for data in selected_data:
            orig_name = data['name']
            new_name = dabaie_compute_name(orig_name, scene)
            base_name = new_name
            counter = 1
            while new_name in existing_names:
                new_name = f"{base_name}.{counter:03d}"
                counter += 1
            existing_names.add(new_name)

            new_eb = edit_bones.new(new_name)
            new_eb.head = data['head']
            new_eb.tail = data['tail']
            new_eb.roll = data['roll']
            new_eb.use_connect = False
            name_map[orig_name] = new_name

        for data in selected_data:
            new_eb = edit_bones.get(name_map[data['name']])
            if new_eb is None:
                continue
            if data['parent_name']:
                parent_bone = edit_bones.get(data['parent_name'])
                if parent_bone is not None:
                    new_eb.parent = parent_bone
            new_eb.use_connect = False

        for data in selected_data:
            orig_eb = edit_bones.get(data['name'])
            new_eb = edit_bones.get(name_map[data['name']])
            if orig_eb is None or new_eb is None:
                continue
            orig_eb.parent = new_eb
            orig_eb.use_connect = False

        collection_msg = ""
        if scene.dabaie_bt_manage_collection:
            coll_name = scene.dabaie_bt_collection_name.strip()
            if coll_name:
                arm_data = arm_obj.data
                coll = arm_data.collections.get(coll_name)
                created = False
                if coll is None:
                    coll = arm_data.collections.new(coll_name)
                    created = True
                assigned = 0
                for new_name in name_map.values():
                    eb = edit_bones.get(new_name)
                    if eb is not None:
                        coll.assign(eb)
                        assigned += 1
                collection_msg = f"，已加入集合 [{coll_name}]"
                if created:
                    collection_msg += "（新建）"
            else:
                collection_msg = "，集合名称为空，已跳过"

        for eb in edit_bones:
            eb.select = False
        for new_name in name_map.values():
            eb = edit_bones.get(new_name)
            if eb is not None:
                eb.select = True

        if original_mode != 'EDIT':
            try:
                bpy.ops.object.mode_set(mode=original_mode)
            except RuntimeError:
                pass

        self.report({'INFO'}, f"已创建 {len(selected_data)} 根父级骨骼{collection_msg}")
        return {'FINISHED'}


# ==================================================================
# 操作符：复制并缩放骨骼 (v2)
# ==================================================================

class DABAIE_OT_copy_and_scale_bones(bpy.types.Operator):
    bl_idname = "dabaie.copy_and_scale_bones"
    bl_label = "复制并缩放骨骼"
    bl_description = ("在姿态模式选中骨骼，原地复制并按前缀改名，"
                      "按比例调整柔性显示尺寸、长度自动反比延长，并归入新骨骼集合")
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        scene = context.scene
        armature_obj = context.active_object
        if not armature_obj or armature_obj.type != 'ARMATURE':
            self.report({'ERROR'}, "请先激活一个骨架物体（Armature）")
            return {'CANCELLED'}
        if armature_obj.mode != 'POSE':
            self.report({'ERROR'}, "请进入【姿态模式】后再执行此操作")
            return {'CANCELLED'}

        selected_pose_bones = context.selected_pose_bones
        if not selected_pose_bones:
            self.report({'ERROR'}, "请至少选中一根骨骼")
            return {'CANCELLED'}

        orig_prefix = scene.dabaie_orig_prefix
        new_prefix = scene.dabaie_new_prefix
        ratio = scene.dabaie_scale_ratio
        if ratio <= 0:
            self.report({'ERROR'}, "厚度缩放比例必须大于 0")
            return {'CANCELLED'}

        mismatched_bones = []
        for pb in selected_pose_bones:
            if not pb.name.startswith(orig_prefix):
                mismatched_bones.append(pb.name)
        if mismatched_bones:
            error_msg = (f"以下骨骼不以 '{orig_prefix}' 开头，操作已取消: "
                         + ", ".join(mismatched_bones[:5]))
            if len(mismatched_bones) > 5:
                error_msg += " ... (等)"
            self.report({'ERROR'}, error_msg)
            return {'CANCELLED'}

        orig_bone_names = [pb.name for pb in selected_pose_bones]

        bpy.ops.object.mode_set(mode='EDIT')
        edit_bones = armature_obj.data.edit_bones

        bpy.ops.armature.select_all(action='DESELECT')
        for name in orig_bone_names:
            if name in edit_bones:
                eb = edit_bones[name]
                eb.select = True
                eb.select_head = True
                eb.select_tail = True

        bpy.ops.armature.duplicate()
        duplicated_edit_bones = context.selected_editable_bones

        new_collection_name = f"{new_prefix}Bones"
        if new_collection_name in armature_obj.data.collections:
            armature_obj.data.collections.remove(
                armature_obj.data.collections[new_collection_name])
        new_collection = armature_obj.data.collections.new(name=new_collection_name)

        processed_count = 0
        for new_eb in duplicated_edit_bones:
            if '.' in new_eb.name:
                orig_name = new_eb.name.rsplit('.', 1)[0]
            else:
                orig_name = new_eb.name

            if orig_name not in edit_bones:
                continue
            orig_eb = edit_bones[orig_name]

            base_name = orig_name[len(orig_prefix):]
            new_eb.name = new_prefix + base_name

            orig_len = orig_eb.length
            if orig_len == 0:
                orig_len = 0.01
            orig_bbone_x = orig_eb.bbone_x
            orig_bbone_z = orig_eb.bbone_z

            new_len = orig_len / ratio
            new_eb.bbone_x = orig_bbone_x * ratio
            new_eb.bbone_z = orig_bbone_z * ratio

            head_vec = new_eb.head.copy()
            tail_vec = new_eb.tail.copy()
            direction = tail_vec - head_vec
            if direction.length > 0:
                direction.normalize()
            else:
                direction = Vector((0, 1, 0))
            new_eb.tail = head_vec + direction * new_len

            new_collection.assign(new_eb)
            processed_count += 1

        bpy.ops.object.mode_set(mode='POSE')
        self.report({'INFO'},
                    f"成功：已复制并调整 {processed_count} 根骨骼，并归入集合 '{new_collection_name}'。")
        return {'FINISHED'}


# ==================================================================
# 操作符：顶点生成骨骼
# ==================================================================

class DABAIE_OT_vertex_to_bones(bpy.types.Operator):
    bl_idname = "dabaie.vertex_to_bones"
    bl_label = "顶点生成骨骼"
    bl_description = ("在网格编辑模式选中顶点，一键在顶点位置生成骨骼：链条或独立模式，"
                      "可选按最近邻排序、骨骼相连、指定方向")
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return obj is not None and obj.type == 'MESH' and obj.mode == 'EDIT'

    def execute(self, context):
        scene = context.scene
        mesh_obj = context.active_object

        bm = bmesh.from_edit_mesh(mesh_obj.data)
        bm.verts.ensure_lookup_table()
        bm.verts.index_update()

        sel = [v for v in bm.verts if v.select]
        if not sel:
            self.report({'WARNING'}, "没有选中任何顶点")
            return {'CANCELLED'}

        sel.sort(key=lambda v: v.index)
        mw = mesh_obj.matrix_world
        points = [mw @ v.co for v in sel]

        if scene.dabaie_vtb_sort == 'NEAREST':
            order = dabaie_nearest_neighbor_order(points)
            points = [points[i] for i in order]

        if scene.dabaie_vtb_mode == 'CHAIN' and len(points) < 2:
            self.report({'WARNING'}, "链条模式至少需要选中 2 个顶点")
            return {'CANCELLED'}

        arm_obj = scene.dabaie_vtb_target
        if arm_obj is None:
            arm_data = bpy.data.armatures.new("VertexArmature")
            arm_obj = bpy.data.objects.new("VertexArmature", arm_data)
            context.collection.objects.link(arm_obj)
            scene.dabaie_vtb_target = arm_obj

        try:
            bpy.ops.object.mode_set(mode='OBJECT')
        except RuntimeError:
            self.report({'ERROR'}, "无法切换到物体模式")
            return {'CANCELLED'}

        dabaie_deselect_all(context)
        try:
            arm_obj.hide_viewport = False
            arm_obj.hide_set(False)
            arm_obj.select_set(True)
            context.view_layer.objects.active = arm_obj
            bpy.ops.object.mode_set(mode='EDIT')
        except RuntimeError:
            self.report({'ERROR'}, "无法进入骨架编辑模式（骨架可能被隐藏或不在当前视图层）")
            dabaie_back_to_mesh_edit(context, mesh_obj)
            return {'CANCELLED'}

        ebs = arm_obj.data.edit_bones
        inv = arm_obj.matrix_world.inverted()
        pts = [inv @ p for p in points]

        prefix = scene.dabaie_vtb_prefix or "Bone_"
        count = 0
        zero_len = 0

        if scene.dabaie_vtb_mode == 'CHAIN':
            prev_bone = None
            for i in range(len(pts) - 1):
                head = pts[i]
                tail = pts[i + 1]
                if (tail - head).length < 1e-6:
                    zero_len += 1
                    continue
                eb = ebs.new(f"{prefix}{i:03d}")
                eb.head = head
                eb.tail = tail
                if scene.dabaie_vtb_connect and prev_bone is not None:
                    eb.parent = prev_bone
                    eb.use_connect = True
                prev_bone = eb
                count += 1
        else:
            n = len(pts)
            if n > 1:
                avg_dist = sum((pts[j] - pts[j - 1]).length
                               for j in range(1, n)) / (n - 1)
            else:
                avg_dist = 1.0
            if avg_dist < 1e-6:
                avg_dist = 1.0

            for i, p in enumerate(pts):
                eb = ebs.new(f"{prefix}{i:03d}")
                eb.head = p
                if scene.dabaie_vtb_axis == 'Z':
                    direction = Vector((0.0, 0.0, 1.0))
                    length = max(avg_dist * scene.dabaie_vtb_length_factor, 1e-5)
                else:
                    if i < n - 1:
                        d = pts[i + 1] - p
                    elif n > 1:
                        d = p - pts[i - 1]
                    else:
                        d = Vector((0.0, 0.0, 1.0))
                    if d.length < 1e-6:
                        d = Vector((0.0, 0.0, 1.0))
                    direction = d.normalized()
                    length = max(d.length * scene.dabaie_vtb_length_factor, 1e-5)
                eb.tail = p + direction * length
                count += 1

        try:
            bpy.ops.object.mode_set(mode='OBJECT')
        except RuntimeError:
            pass

        dabaie_back_to_mesh_edit(context, mesh_obj)

        msg = f"已在 [{arm_obj.name}] 中生成 {count} 根骨骼"
        if zero_len:
            msg += f"（跳过 {zero_len} 根零长度骨骼）"
        self.report({'INFO'}, msg)
        return {'FINISHED'}


# ==================================================================
# 操作符：骨骼顶点智能绑定
# ==================================================================

class DABAIE_OT_bind_bones_to_vertices(bpy.types.Operator):
    bl_idname = "dabaie.bind_bones_to_vertices"
    bl_label = "骨骼顶点智能绑定"
    bl_description = ("在姿态模式选中目标骨骼，指定代理模型，按空间最近邻把骨骼头绑定到最近顶点，"
                      "生成 COPY_LOCATION 顶点组约束（世界空间）")
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        scene = context.scene
        mesh_obj = scene.dabaie_proxy_mesh
        if not mesh_obj or mesh_obj.type != 'MESH':
            self.report({'ERROR'}, "请选择一个有效的网格模型")
            return {'CANCELLED'}

        armature_obj = context.active_object
        if not armature_obj or armature_obj.type != 'ARMATURE':
            self.report({'ERROR'}, "请先激活一个骨架物体（Armature）")
            return {'CANCELLED'}

        selected_bones = [bone for bone in armature_obj.pose.bones if bone.bone.select]
        if not selected_bones:
            self.report({'ERROR'}, "请在姿态模式下选中至少一根骨骼")
            return {'CANCELLED'}

        mesh_world_matrix = mesh_obj.matrix_world
        vertices = mesh_obj.data.vertices
        kd = mathutils.kdtree.KDTree(len(vertices))
        for i, v in enumerate(vertices):
            kd.insert(mesh_world_matrix @ v.co, i)
        kd.balance()

        if scene.dabaie_create_all_vgroups:
            for i in range(len(vertices)):
                vg_name = f"v_{i:04d}"
                if vg_name not in mesh_obj.vertex_groups:
                    mesh_obj.vertex_groups.new(name=vg_name)

        bind_count = 0
        armature_world_matrix = armature_obj.matrix_world

        for pbone in selected_bones:
            bone_head_world = armature_world_matrix @ pbone.head
            co, index, dist = kd.find(bone_head_world)
            vg_name = f"v_{index:04d}"

            if vg_name not in mesh_obj.vertex_groups:
                vg = mesh_obj.vertex_groups.new(name=vg_name)
                vg.add([index], 1.0, 'REPLACE')
            else:
                vg = mesh_obj.vertex_groups[vg_name]
                is_in_group = any(g.group == vg.index for g in vertices[index].groups)
                if not is_in_group:
                    vg.add([index], 1.0, 'REPLACE')

            for c in list(pbone.constraints):
                if c.name == "VertexBind":
                    pbone.constraints.remove(c)

            constraint = pbone.constraints.new('COPY_LOCATION')
            constraint.name = "VertexBind"
            constraint.target = mesh_obj
            constraint.subtarget = vg_name
            constraint.target_space = 'WORLD'
            constraint.owner_space = 'WORLD'
            constraint.use_x = True
            constraint.use_y = True
            constraint.use_z = True

            bind_count += 1

        self.report({'INFO'}, f"完成！智能匹配并绑定了 {bind_count} 根骨骼到最近顶点。")
        return {'FINISHED'}


# ==================================================================
# 操作符：全选控制器关键帧
# ==================================================================

class DABAIE_OT_key_all_selected(bpy.types.Operator):
    bl_idname = "dabaie.key_all_selected"
    bl_label = "全选控制器关键帧"
    bl_description = ("在姿态模式选中控制器并已有激活 Action，"
                      "在这些骨骼被 K 过帧的所有帧上为它们批量插入位置/旋转/缩放关键帧")
    bl_options = {'REGISTER', 'UNDO'}

    def _get_bone_keyframe_frames(self, bone_name, action):
        frames = set()
        prefix = f'pose.bones["{bone_name}"]'
        for fc in dabaie_iter_action_fcurves(action):
            if fc.data_path.startswith(prefix):
                for kp in fc.keyframe_points:
                    frames.add(int(round(kp.co[0])))
        return frames

    def execute(self, context):
        arm_obj = context.object
        if arm_obj is None or arm_obj.type != 'ARMATURE':
            self.report({'ERROR'}, "请先选中一个骨架对象（Armature）")
            return {'CANCELLED'}
        if context.mode != 'POSE':
            self.report({'ERROR'}, "请在姿态模式（Pose Mode）下运行")
            return {'CANCELLED'}

        arm_ad = arm_obj.animation_data
        if not arm_ad or not arm_ad.action:
            self.report({'ERROR'}, "当前骨架没有激活的 Action（动画数据）")
            return {'CANCELLED'}
        action = arm_ad.action
        print(f"当前 Action: {action.name}")

        selected_bones = [b for b in arm_obj.pose.bones if b.bone.select]
        if not selected_bones:
            self.report({'ERROR'}, "没有选中任何骨骼控制器")
            return {'CANCELLED'}

        all_frames = set()
        for bone in selected_bones:
            bf = self._get_bone_keyframe_frames(bone.name, action)
            if bf:
                all_frames.update(bf)

        all_frames = sorted(all_frames)
        if not all_frames:
            self.report({'WARNING'}, "未找到任何关键帧")
            return {'CANCELLED'}

        original_frame = context.scene.frame_current
        for frame in all_frames:
            context.scene.frame_set(frame)
            for bone in selected_bones:
                bone.keyframe_insert(data_path="location", frame=frame)
                if bone.rotation_mode == 'QUATERNION':
                    bone.keyframe_insert(data_path="rotation_quaternion", frame=frame)
                elif bone.rotation_mode == 'AXIS_ANGLE':
                    bone.keyframe_insert(data_path="rotation_axis_angle", frame=frame)
                else:
                    bone.keyframe_insert(data_path="rotation_euler", frame=frame)
                bone.keyframe_insert(data_path="scale", frame=frame)

        context.scene.frame_set(original_frame)
        self.report({'INFO'}, f"完成：在 {len(all_frames)} 个帧上为 {len(selected_bones)} 根控制器插入关键帧")
        return {'FINISHED'}


# ==================================================================
# 操作符：切换中英文
# ==================================================================

class DABAIE_OT_toggle_language(bpy.types.Operator):
    bl_idname = "dabaie.toggle_language"
    bl_label = "切换中英文"
    bl_description = "一键切换 Blender 界面语言（中文 ⇄ 英文）"
    bl_options = {'REGISTER'}

    def execute(self, context):
        prefs = context.preferences.view
        if prefs.language in {'zh_HANS', 'zh_HANT'}:
            prefs.language = 'en_US'
            self.report({'INFO'}, "Switched to English")
        else:
            prefs.language = 'zh_HANS'
            self.report({'INFO'}, "已切换为简体中文")
        bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
        return {'FINISHED'}


# ==================================================================
# UI 面板
# ==================================================================

class DABAIE_PT_main(bpy.types.Panel):
    bl_label = "大白鹅绑定工具箱"
    bl_idname = "DABAIE_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "大白鹅绑定工具箱"

    def draw(self, context):
        layout = self.layout
        scene = context.scene

        layout.label(text="悬停按钮查看使用提示", icon='INFO')


class DABAIE_PT_constraints(bpy.types.Panel):
    bl_label = "约束工具"
    bl_parent_id = "DABAIE_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    def draw(self, context):
        layout = self.layout
        scene = context.scene

        # 镜像约束
        box = layout.box()
        row = box.row(align=True)
        row.operator("dabaie.mirror_constraints", text="镜像约束 L⇄R",
                     icon='ARROW_LEFTRIGHT')
        row.prop(scene, "dabaie_show_mirror", text="", icon='SETTINGS', toggle=True)
        if scene.dabaie_show_mirror:
            col = box.column(align=True)
            col.prop(scene, "dabaie_mirror_replace_target")
            col.prop(scene, "dabaie_mirror_clear_existing")
            col.prop(scene, "dabaie_mirror_include_muted")

        # 批量一对一约束
        box = layout.box()
        row = box.row(align=True)
        row.operator("dabaie.constrain_def_by_ctrl", text="批量一对一约束",
                     icon='LINKED')
        row.prop(scene, "dabaie_show_onetoone", text="", icon='SETTINGS', toggle=True)
        if scene.dabaie_show_onetoone:
            col = box.column(align=True)
            col.prop(scene, "dabaie_def_prefix")
            col.prop(scene, "dabaie_ctrl_prefix")
            col.prop(scene, "dabaie_constraint_type")

        # 批量修改影响值
        box = layout.box()
        row = box.row(align=True)
        row.operator("dabaie.batch_apply_influence", text="批量修改影响值",
                     icon='CONSTRAINT')
        row.prop(scene, "dabaie_show_influence", text="", icon='SETTINGS', toggle=True)
        if scene.dabaie_show_influence:
            col = box.column(align=True)
            col.prop(scene, "dabaie_batch_constraint_type")
            col.prop(scene, "dabaie_influence_value", slider=True)
            qrow = col.row(align=True)
            qrow.operator("dabaie.batch_set_influence", text="0%").value = 0.0
            qrow.operator("dabaie.batch_set_influence", text="25%").value = 0.25
            qrow.operator("dabaie.batch_set_influence", text="50%").value = 0.5
            qrow.operator("dabaie.batch_set_influence", text="75%").value = 0.75
            qrow.operator("dabaie.batch_set_influence", text="100%").value = 1.0
            col.operator("dabaie.batch_read_influence", text="从选中骨骼读取影响值",
                         icon='IMPORT')

        # 关节链约束
        layout.operator("dabaie.joint_chain", text="关节链约束 (IK/FK)",
                        icon='CONSTRAINT_BONE')

        # 清除约束
        layout.operator("dabaie.clean_constraints", text="清除选中骨骼约束", icon='X')

        # 约束旋转捕获
        capture_on = dabaie_capture_is_enabled()
        if scene.dabaie_capture_enabled != capture_on:
            scene.dabaie_capture_enabled = capture_on
        layout.operator("dabaie.toggle_capture_rotation",
                        text=("停用约束旋转捕获" if capture_on else "启用约束旋转捕获"),
                        icon=('RADIOBUT_ON' if capture_on else 'RADIOBUT_OFF'))


class DABAIE_PT_bones(bpy.types.Panel):
    bl_label = "骨骼工具"
    bl_parent_id = "DABAIE_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    def draw(self, context):
        layout = self.layout
        scene = context.scene

        # 复制骨骼为父级
        box = layout.box()
        row = box.row(align=True)
        row.operator("dabaie.duplicate_as_parent", text="复制骨骼为父级",
                     icon='DUPLICATE')
        row.prop(scene, "dabaie_show_parent", text="", icon='SETTINGS', toggle=True)
        if scene.dabaie_show_parent:
            col = box.column(align=True)
            col.prop(scene, "dabaie_bt_naming_mode")
            col.prop(scene, "dabaie_bt_name_text")
            if scene.dabaie_bt_naming_mode in {'REPLACE_PREFIX', 'REPLACE_SUFFIX'}:
                col.prop(scene, "dabaie_bt_replace_text")
            col.prop(scene, "dabaie_bt_manage_collection")
            sub = col.column(align=True)
            sub.enabled = scene.dabaie_bt_manage_collection
            sub.prop(scene, "dabaie_bt_collection_name")

        # 复制并缩放骨骼
        box = layout.box()
        row = box.row(align=True)
        row.operator("dabaie.copy_and_scale_bones", text="复制并缩放骨骼",
                     icon='BONE_DATA')
        row.prop(scene, "dabaie_show_copyscale", text="", icon='SETTINGS', toggle=True)
        if scene.dabaie_show_copyscale:
            col = box.column(align=True)
            col.prop(scene, "dabaie_orig_prefix")
            col.prop(scene, "dabaie_new_prefix")
            col.prop(scene, "dabaie_scale_ratio")

        # 顶点生成骨骼
        box = layout.box()
        row = box.row(align=True)
        row.operator("dabaie.vertex_to_bones", text="顶点生成骨骼", icon='BONE_DATA')
        row.prop(scene, "dabaie_show_vertex", text="", icon='SETTINGS', toggle=True)
        if scene.dabaie_show_vertex:
            col = box.column(align=True)
            col.prop(scene, "dabaie_vtb_target")
            col.prop(scene, "dabaie_vtb_mode")
            col.prop(scene, "dabaie_vtb_sort")
            if scene.dabaie_vtb_mode == 'CHAIN':
                col.prop(scene, "dabaie_vtb_connect")
            else:
                col.prop(scene, "dabaie_vtb_axis")
                col.prop(scene, "dabaie_vtb_length_factor")
            col.prop(scene, "dabaie_vtb_prefix")


class DABAIE_PT_binding(bpy.types.Panel):
    bl_label = "绑定工具"
    bl_parent_id = "DABAIE_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    def draw(self, context):
        layout = self.layout
        scene = context.scene

        box = layout.box()
        row = box.row(align=True)
        row.operator("dabaie.bind_bones_to_vertices", text="骨骼顶点智能绑定",
                     icon='LINKED')
        row.prop(scene, "dabaie_show_bind", text="", icon='SETTINGS', toggle=True)
        if scene.dabaie_show_bind:
            col = box.column(align=True)
            col.prop(scene, "dabaie_proxy_mesh")
            col.prop(scene, "dabaie_create_all_vgroups")


class DABAIE_PT_animation(bpy.types.Panel):
    bl_label = "动画工具"
    bl_parent_id = "DABAIE_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    def draw(self, context):
        layout = self.layout
        layout.operator("dabaie.key_all_selected", text="全选控制器关键帧",
                        icon='KEY_HLT')


class DABAIE_PT_system(bpy.types.Panel):
    bl_label = "系统工具"
    bl_parent_id = "DABAIE_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    def draw(self, context):
        layout = self.layout
        layout.operator("dabaie.toggle_language", text="切换中英文", icon='WORLD')


# ==================================================================
# 注册
# ==================================================================

classes = (
    DABAIE_OT_mirror_constraints,
    DABAIE_OT_constrain_def_by_ctrl,
    DABAIE_OT_batch_apply_influence,
    DABAIE_OT_batch_set_influence,
    DABAIE_OT_batch_read_influence,
    DABAIE_OT_joint_chain,
    DABAIE_OT_clean_constraints,
    DABAIE_OT_toggle_capture_rotation,
    DABAIE_OT_duplicate_as_parent,
    DABAIE_OT_copy_and_scale_bones,
    DABAIE_OT_vertex_to_bones,
    DABAIE_OT_bind_bones_to_vertices,
    DABAIE_OT_key_all_selected,
    DABAIE_OT_toggle_language,
    DABAIE_PT_main,
    DABAIE_PT_constraints,
    DABAIE_PT_bones,
    DABAIE_PT_binding,
    DABAIE_PT_animation,
    DABAIE_PT_system,
)


def register():
    register_properties()
    for cls in classes:
        bpy.utils.register_class(cls)


def unregister():
    if dabaie_capture_is_enabled():
        for h in list(bpy.app.handlers.depsgraph_update_post):
            if h.__name__ == "dabaie_capture_constraint_rot":
                bpy.app.handlers.depsgraph_update_post.remove(h)
    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)
    unregister_properties()


if __name__ == "__main__":
    register()
