Skip to content

ADR-145: 多模块协同预设 — 一键启用组合姿态

日期: 2026-07-17 状态: ✅ P1 已实施(类型定义 + applyMotionPreset + UI 卡片 + Go 侧 .mcpreset.json CRUD + 5 语言 i18n) 背景: 用户需要「坐姿」「站姿」「舞蹈准备」等复合姿态,涉及多个模块协同(如 body-posture + riding-model + position-offset 同时启用并设定特定参数值)。当前需手动逐个开关、调参,缺一键应用能力。


一、问题

当前操作路径(示意,模块列表以注册表为准)

动作覆盖 → 身体姿态(body-posture: tilt/bend/twist)
         → 骑行模型(riding-model: saddleHeight/pedalAngle/preset)
         → 位置偏移(position-offset: sideShift/vertShift/depthShift)
         → 摇摆运动(sway-motion: amplitude/frequency)
         → 手对称(hand-symmetry: 镜像参数)
         → 手指姿势(finger-pose: 预设/自定义)

痛点

痛点说明
操作繁琐每次切换场景需手动调 5-10 个参数
无记忆无法保存/恢复一组满意的姿态组合
无分享无法将预设导出给其他模型或用户
模型间不通用不同模型骨骼映射不同,预设需适配

二、方案

预设数据结构

类型定义位置:motion/motion-modules/preset-types.ts(紧邻 MotionModuleStateMotionOverrideModule)。

MotionPreset 是 DTO(数据传输对象),与 ModelInstance.motionOverrideModules: MotionModuleState[] 形状不同:

结构形状用途
MotionModuleState[][{ id, enabled, params }]运行时 per-model 状态
MotionPreset['modules']{ [moduleId]: { enabled, params } }预设序列化 DTO(按 ID 快查)

转换函数 motionModuleStateToPresetModules(states) / presetModulesToMotionModuleState(modules) 放在 preset-types.ts 中。

ts
// motion/motion-modules/preset-types.ts
import type { MotionModuleState, ParamValue } from '@/core/types';

export interface MotionPreset {
    id: string;
    name: string;
    description?: string;
    /** 各模块的参数快照(DTO 格式,以 moduleId 为 key 方便快查) */
    modules: {
        [moduleId: string]: {
            enabled: boolean;
            params: Record<string, ParamValue>;
        };
    };
    /** 可选:模型骨骼映射(用于跨模型适配) */
    boneMapping?: Record<string, string>;
}

/** MotionModuleState[] → MotionPreset['modules'] */
export function modulesToPresetMap(
    states: MotionModuleState[]
): MotionPreset['modules'] { /* ... */ }

/** MotionPreset['modules'] → MotionModuleState[] */
export function presetMapToModules(
    map: MotionPreset['modules']
): MotionModuleState[] { /* ... */ }

示例:坐姿预设

json
{
    "id": "seated",
    "name": "坐姿",
    "modules": {
        "body-posture": {
            "enabled": true,
            "params": { "tilt": -5, "bend": 15, "twist": 0 }
        },
        "riding-model": {
            "enabled": true,
            "params": { "preset": "bicycle", "saddleHeight": 0.7, "pedalAngle": 180 }
        },
        "position-offset": {
            "enabled": true,
            "params": { "vertShift": -8, "depthShift": 5 }
        }
    }
}

应用流程

ts
// 核心函数:applyMotionPreset(modelId, preset)
// 命名加「Motion」前缀,避免与 model-preset.ts 的 applyPresetFromLib 混淆
用户选预设 → 遍历 preset.modules → 对每个模块:
  const mod = preset.modules[moduleId];
  if (!mod) continue;
  setModuleEnabled(modelId, moduleId, mod.enabled);
  for (const [key, value] of Object.entries(mod.params)) {
    setModuleParam(modelId, moduleId, key, value);
  }
  createModule(moduleId, modelId)?.enable();  // 触发烘焙(模块不存在时返回 null 跳过)

预设存储

位置说明
ModelInstance.motionPresets?: MotionPreset[]按模型存储(per-model),优先级:per-model > 全局
localStorage 或全局预设库跨模型共享(可选),同名时 per-model 覆盖全局
序列化到 scene-serialize.ts随场景保存/加载,motionPresets 字段加在 serializeModel

三、UI 设计

入口

motion-override-levels.tsbuildMotionOverrideSchema() 中,模块列表卡片(override:modules)上方增加一张独立「预设」卡片(与 override:advanced 同级,而非放在模块列表卡片内部):

┌─────────────────────────────────┐
│  预设                            │
│  ┌───────────────────────────┐  │
│  │ 坐姿          ▸ 应用/保存 │  │
│  │ 站姿          ▸          │  │
│  │ 舞蹈准备      ▸          │  │
│  │ [+ 新建预设]              │  │
│  └───────────────────────────┘  │
├─────────────────────────────────┤
│  身体姿态               ˄  ⋮   │
│  骑行模型               ˄  ⋮   │
│  ...                           │
└─────────────────────────────────┘

按钮

按钮行为
应用遍历 preset.modules,逐模块 setParam + enable
保存快照当前所有 enabled 模块的参数到 preset
新建清空当前预设,填入当前模块状态
删除移除该预设

预设编辑

简化方案(P1 P2 阶段):预设编辑只支持「保存当前全部模块状态」→「完整应用」,不支持分模块编辑。用户如需调整,先应用预设,再通过模块列表微调参数,最后重新保存。

后续扩展(P3+):如需分模块编辑预设,可新增「预设编辑器」子页,左侧列出预设中包含的模块,右侧借用 buildModuleParamLevel(moduleId) 渲染单个模块的参数(与现有模块参数子页渲染方式一致)。


四、实施分期

阶段内容验收
P1定义 MotionPreset 类型 + applyMotionPreset() 函数tsc 通过
P1预设卡片 UI + 应用/保存/新建/删除(保存时检查 ≤10 个,超出提示「请删除旧预设」)交互可用
P2持久化到 ModelInstance.motionPresets + scene-serialize.ts保存→重载后预设仍在
P2applyMotionPreset 调用前 pushUndoSnapshot(),使应用预设可撤销回退应用预设后 Ctrl+Z 可恢复
P3跨模型骨骼映射(boneMapping不同模型间可复用预设
P3预设导出/导入(JSON 文件)可分享给其他用户

五、风险与缓解

风险缓解
预设参数与当前模块状态冲突(用户手动调参后应用预设)应用预设时全量覆盖,不尝试合并
模型缺失预设中的骨骼(如 riding-model 的 左ひざ 不存在)claimBones 已自动跳过缺失骨骼;createModule 返回 null 时该模块静默跳过
预设过多导致 UI 列表过长保存按钮中检查 motionPresets.length >= 10,超出时提示「请删除旧预设」
全局预设与 per-model 预设同名per-model 优先级高于全局,同名时 per-model 覆盖全局(全局作为 fallback 只读库)