Files
cc-3-8-x-mcp/cli/src/anim-primitives.js
T
furao 14c5b00f14 Initial public release: cc-3-8-x-mcp
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.
2026-06-06 11:33:19 +08:00

379 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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.ChannelTrack → 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.ColorTrack4 通道 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,
};