mirror of
https://github.com/HappyLifeOk/cc-3-8-x-mcp.git
synced 2026-06-10 17:56:47 +00:00
14c5b00f14
Cocos Creator 3.8.x MCP bridge extension with a built-in offline CLI. Components: - Editor extension: in-process MCP server exposing scene / asset-db / preview / local / editor-process-control tools - stdio router: aggregates multiple editor instances on one machine, with shortName dedup - offline CLI (cocos-mcp-cli): headless prefab read/write + a wrapper around the Cocos CLI build Pure Node.js, zero third-party dependencies. Licensed under Apache-2.0.
379 lines
15 KiB
JavaScript
379 lines
15 KiB
JavaScript
// ============================================================
|
||
// CC3 AnimationClip (.anim) 对象构建原语(纯 CJS,零三方依赖)
|
||
//
|
||
// .anim 文件和 .prefab 一样是 JSON 数组 + `__id__` 交叉引用,
|
||
// 复用 parsePrefab / writePrefab 解析写入。
|
||
//
|
||
// 但 .anim 内部对象类型(AnimationClip / Track / Curve / Channel)
|
||
// 有自己的 schema 规范,最容易踩的坑:
|
||
//
|
||
// ✅ cc.animation.RealTrack → 单通道 → `_channel: {__id__: X}` (单数)
|
||
// ✅ cc.animation.ObjectTrack → 单通道 → `_channel: {__id__: X}` (单数)
|
||
// ✅ cc.animation.VectorTrack → 多通道 → `_channels: [x, y, z]` (复数数组)
|
||
// ✅ cc.animation.ColorTrack → 多通道 → `_channels: [r, g, b, a]` (复数数组)
|
||
//
|
||
// 写错字段名(比如给 RealTrack 写 `_channels: [...]`)会导致 CC3 编辑器
|
||
// 按 schema 验证时直接忽略整条轨道,表现为「anim 文件里有数据但编辑器
|
||
// 不显示关键帧、运行时也不播」——历史踩过的坑,见 anim-schema.md。
|
||
//
|
||
// 用本文件里的 make* 工厂函数,保证字段名一定写对。
|
||
// ============================================================
|
||
|
||
'use strict';
|
||
|
||
const { ref } = require('./primitives.js');
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 插值模式常量
|
||
// 对应 cc.RealCurve.interpolationMode 枚举
|
||
// ─────────────────────────────────────────────
|
||
const InterpolationMode = Object.freeze({
|
||
LINEAR: 0, // 线性插值,两关键帧之间平滑过渡
|
||
CONSTANT: 1, // 常量插值,保持当前值直到下一帧瞬变
|
||
CUBIC: 2, // 三次插值,带缓动曲线
|
||
});
|
||
|
||
// ─────────────────────────────────────────────
|
||
// Extrapolation 模式常量
|
||
// 对应 cc.RealCurve.preExtrapolation / postExtrapolation
|
||
// ─────────────────────────────────────────────
|
||
const Extrapolation = Object.freeze({
|
||
LINEAR: 0,
|
||
CLAMP: 1, // 最常用:首帧之前保持首帧值,末帧之后保持末帧值
|
||
REPEAT: 2,
|
||
PINGPONG: 3,
|
||
});
|
||
|
||
// ─────────────────────────────────────────────
|
||
// RealKeyframeValue:单个浮点关键帧
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {object} opts
|
||
* @param {number} opts.value
|
||
* @param {number} [opts.interpolationMode=CONSTANT] 0=LINEAR 1=CONSTANT 2=CUBIC
|
||
* @param {number} [opts.easingMethod=0] 0=Linear,其余见 cc.EasingMethod
|
||
*/
|
||
function makeRealKeyframe(opts) {
|
||
const { value, interpolationMode = InterpolationMode.CONSTANT, easingMethod = 0 } = opts;
|
||
return {
|
||
__type__: 'cc.RealKeyframeValue',
|
||
interpolationMode,
|
||
tangentWeightMode: 0,
|
||
value,
|
||
rightTangent: 0,
|
||
rightTangentWeight: 1,
|
||
leftTangent: 0,
|
||
leftTangentWeight: 1,
|
||
easingMethod,
|
||
__editorExtras__: null,
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.RealCurve:浮点曲线
|
||
// times/values 一一对应,长度必须相同
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {object} opts
|
||
* @param {number[]} opts.times - 递增时间点(秒)
|
||
* @param {object[]} opts.values - RealKeyframeValue 对象数组,与 times 同长
|
||
* @param {number} [opts.preExtrapolation=1] CLAMP
|
||
* @param {number} [opts.postExtrapolation=1] CLAMP
|
||
*/
|
||
function makeRealCurve(opts) {
|
||
const { times, values, preExtrapolation = Extrapolation.CLAMP, postExtrapolation = Extrapolation.CLAMP } = opts;
|
||
if (times.length !== values.length) {
|
||
throw new Error(`makeRealCurve: times.length (${times.length}) !== values.length (${values.length})`);
|
||
}
|
||
return {
|
||
__type__: 'cc.RealCurve',
|
||
_times: times,
|
||
_values: values,
|
||
preExtrapolation,
|
||
postExtrapolation,
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.ObjectCurve:对象引用曲线(用于 spriteFrame 等资产序列)
|
||
// values 是资产引用对象数组(`{ __uuid__, __expectedType__ }`)
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {object} opts
|
||
* @param {number[]} opts.times
|
||
* @param {object[]} opts.values - 资产引用对象
|
||
*/
|
||
function makeObjectCurve(opts) {
|
||
const { times, values, preExtrapolation = Extrapolation.CLAMP, postExtrapolation = Extrapolation.CLAMP } = opts;
|
||
if (times.length !== values.length) {
|
||
throw new Error(`makeObjectCurve: times.length (${times.length}) !== values.length (${values.length})`);
|
||
}
|
||
return {
|
||
__type__: 'cc.ObjectCurve',
|
||
_times: times,
|
||
_values: values,
|
||
preExtrapolation,
|
||
postExtrapolation,
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.animation.Channel:Track → Curve 的中间层
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {number} curveIdx - RealCurve/ObjectCurve 在 objects 数组里的下标
|
||
*/
|
||
function makeChannel(curveIdx) {
|
||
return {
|
||
__type__: 'cc.animation.Channel',
|
||
_curve: ref(curveIdx),
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.animation.HierarchyPath / ComponentPath / TrackPath
|
||
// 用于定位轨道的目标节点与属性
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {string} path - 节点层级路径,如 "n4" 或 "content/titleBar";根节点自身写 ""
|
||
*/
|
||
function makeHierarchyPath(path) {
|
||
return { __type__: 'cc.animation.HierarchyPath', path };
|
||
}
|
||
|
||
/**
|
||
* @param {string} component - 组件类型,如 "cc.UIOpacity" / "cc.Sprite"
|
||
*/
|
||
function makeComponentPath(component) {
|
||
return { __type__: 'cc.animation.ComponentPath', component };
|
||
}
|
||
|
||
/**
|
||
* @param {unknown[]} parts - 混合 HierarchyPath/ComponentPath 引用与属性字符串
|
||
* 例:[ref(hierIdx), ref(compIdx), "opacity"]
|
||
* 或根节点属性:[ref(compIdx), "position"](无 hierarchy path)
|
||
*/
|
||
function makeTrackPath(parts) {
|
||
return { __type__: 'cc.animation.TrackPath', _paths: parts };
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.animation.RealTrack(单通道浮点轨道)
|
||
// ⚠️ 字段必须是 `_channel`(单数),不是 `_channels`
|
||
// ⚠️ 写错会被 CC3 编辑器 schema 验证忽略,轨道形同虚设
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {number} trackPathIdx - TrackPath 在 objects 数组里的下标
|
||
* @param {number} channelIdx - Channel 在 objects 数组里的下标
|
||
*/
|
||
function makeRealTrack(trackPathIdx, channelIdx) {
|
||
return {
|
||
__type__: 'cc.animation.RealTrack',
|
||
_binding: {
|
||
__type__: 'cc.animation.TrackBinding',
|
||
path: ref(trackPathIdx),
|
||
proxy: null,
|
||
},
|
||
_channel: ref(channelIdx),
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.animation.ObjectTrack(单通道对象轨道,常用于 spriteFrame 序列帧)
|
||
// ⚠️ 字段必须是 `_channel`(单数)
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {number} trackPathIdx
|
||
* @param {number} channelIdx
|
||
*/
|
||
function makeObjectTrack(trackPathIdx, channelIdx) {
|
||
return {
|
||
__type__: 'cc.animation.ObjectTrack',
|
||
_binding: {
|
||
__type__: 'cc.animation.TrackBinding',
|
||
path: ref(trackPathIdx),
|
||
proxy: null,
|
||
},
|
||
_channel: ref(channelIdx),
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.animation.VectorTrack(多通道,x/y/z 或 x/y/z/w)
|
||
// ⚠️ 字段必须是 `_channels`(复数,数组),且必须提供 `_nComponents`
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {number} trackPathIdx
|
||
* @param {number[]} channelIndices - 各轴 Channel 下标数组(长度 = nComponents)
|
||
* @param {number} nComponents - 2(Vec2) / 3(Vec3) / 4(Vec4/Quat)
|
||
*/
|
||
function makeVectorTrack(trackPathIdx, channelIndices, nComponents) {
|
||
if (channelIndices.length !== nComponents) {
|
||
throw new Error(`makeVectorTrack: channelIndices.length (${channelIndices.length}) !== nComponents (${nComponents})`);
|
||
}
|
||
return {
|
||
__type__: 'cc.animation.VectorTrack',
|
||
_binding: {
|
||
__type__: 'cc.animation.TrackBinding',
|
||
path: ref(trackPathIdx),
|
||
proxy: null,
|
||
},
|
||
_channels: channelIndices.map(ref),
|
||
_nComponents: nComponents,
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.animation.ColorTrack(4 通道 r/g/b/a)
|
||
// ⚠️ 字段必须是 `_channels`(复数,数组),无 `_nComponents`
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {number} trackPathIdx
|
||
* @param {number[]} channelIndices - [rIdx, gIdx, bIdx, aIdx]
|
||
*/
|
||
function makeColorTrack(trackPathIdx, channelIndices) {
|
||
if (channelIndices.length !== 4) {
|
||
throw new Error(`makeColorTrack: expected 4 channel indices, got ${channelIndices.length}`);
|
||
}
|
||
return {
|
||
__type__: 'cc.animation.ColorTrack',
|
||
_binding: {
|
||
__type__: 'cc.animation.TrackBinding',
|
||
path: ref(trackPathIdx),
|
||
proxy: null,
|
||
},
|
||
_channels: channelIndices.map(ref),
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.AnimationClipAdditiveSettings(每个 clip 尾巴一份)
|
||
// ─────────────────────────────────────────────
|
||
function makeAdditiveSettings() {
|
||
return {
|
||
__type__: 'cc.AnimationClipAdditiveSettings',
|
||
enabled: false,
|
||
refClip: null,
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.AnimationClip 文件头(objects[0])
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {object} opts
|
||
* @param {string} opts.name
|
||
* @param {number} opts.sample - 采样帧率
|
||
* @param {number} opts.duration - 秒
|
||
* @param {number} [opts.speed=1]
|
||
* @param {number} [opts.wrapMode=1] 1=Normal, 2=Loop, 22=PingPong, 36=PingPongLoop
|
||
* @param {number} opts.hash - 哈希值(通常 hashString(name))
|
||
* @param {number[]} opts.trackIndices - 所有 Track 在 objects 数组里的下标
|
||
* @param {number} opts.additiveIdx - AdditiveSettings 下标
|
||
* @param {number[]} [opts.embeddedPlayerIndices=[]] - EmbeddedPlayer 下标
|
||
*/
|
||
function makeAnimationClip(opts) {
|
||
const {
|
||
name,
|
||
sample,
|
||
duration,
|
||
speed = 1,
|
||
wrapMode = 1,
|
||
hash,
|
||
trackIndices,
|
||
additiveIdx,
|
||
embeddedPlayerIndices = [],
|
||
} = opts;
|
||
return {
|
||
__type__: 'cc.AnimationClip',
|
||
_name: name,
|
||
_objFlags: 0,
|
||
__editorExtras__: { embeddedPlayerGroups: [] },
|
||
_native: '',
|
||
sample,
|
||
speed,
|
||
wrapMode,
|
||
enableTrsBlending: false,
|
||
_duration: duration,
|
||
_hash: hash,
|
||
_tracks: trackIndices.map(ref),
|
||
_exoticAnimation: null,
|
||
_events: [],
|
||
_embeddedPlayers: embeddedPlayerIndices.map(ref),
|
||
_additiveSettings: ref(additiveIdx),
|
||
_auxiliaryCurveEntries: [],
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// cc.animation.EmbeddedPlayer / EmbeddedAnimationClipPlayable
|
||
// 用于 movieclip 这类「主 clip 触发节点自己 Animation 播子 clip」的结构
|
||
// ─────────────────────────────────────────────
|
||
/**
|
||
* @param {object} opts
|
||
* @param {number} opts.begin - 主 clip 时间线上子 clip 开始秒
|
||
* @param {number} opts.end - 主 clip 时间线上子 clip 结束秒
|
||
* @param {number} opts.playableIdx - EmbeddedAnimationClipPlayable 下标
|
||
*/
|
||
function makeEmbeddedPlayer(opts) {
|
||
const { begin, end, playableIdx } = opts;
|
||
return {
|
||
__type__: 'cc.animation.EmbeddedPlayer',
|
||
begin,
|
||
end,
|
||
reconciledSpeed: false,
|
||
playable: ref(playableIdx),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @param {object} opts
|
||
* @param {string} opts.path - 目标节点路径(相对 clip 所在节点)
|
||
* @param {string} opts.clipUuid - 子 clip 资产 UUID
|
||
*/
|
||
function makeEmbeddedAnimationClipPlayable(opts) {
|
||
const { path, clipUuid } = opts;
|
||
return {
|
||
__type__: 'cc.animation.EmbeddedAnimationClipPlayable',
|
||
path,
|
||
clip: {
|
||
__uuid__: clipUuid,
|
||
__expectedType__: 'cc.AnimationClip',
|
||
},
|
||
};
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// 哈希函数:生成 AnimationClip._hash
|
||
// 用简单字符串哈希,只需保证同名 clip 得到同一 hash 即可
|
||
// ─────────────────────────────────────────────
|
||
function hashString(s) {
|
||
let hash = 0;
|
||
for (let i = 0; i < s.length; i++) {
|
||
hash = ((hash << 5) - hash) + s.charCodeAt(i);
|
||
hash |= 0;
|
||
}
|
||
return Math.abs(hash);
|
||
}
|
||
|
||
module.exports = {
|
||
InterpolationMode,
|
||
Extrapolation,
|
||
makeRealKeyframe,
|
||
makeRealCurve,
|
||
makeObjectCurve,
|
||
makeChannel,
|
||
makeHierarchyPath,
|
||
makeComponentPath,
|
||
makeTrackPath,
|
||
makeRealTrack,
|
||
makeObjectTrack,
|
||
makeVectorTrack,
|
||
makeColorTrack,
|
||
makeAdditiveSettings,
|
||
makeAnimationClip,
|
||
makeEmbeddedPlayer,
|
||
makeEmbeddedAnimationClipPlayable,
|
||
hashString,
|
||
};
|