From 36ee2ef5ff671f366f9e3a82e9a3c205c088064e Mon Sep 17 00:00:00 2001 From: changdunwei Date: Mon, 22 Jun 2026 18:13:56 +0800 Subject: [PATCH] feat(assets): add animation mask tools --- src/api/assets/assets.ts | 124 +++++++++++++++ src/api/assets/schema.ts | 19 +++ src/core/assets/@types/public.d.ts | 18 +++ src/core/assets/animation-mask-utils.ts | 168 ++++++++++++++++++++ src/core/assets/animation-mask.ts | 164 +++++++++++++++++++ src/core/assets/test/animation-mask.test.ts | 80 ++++++++++ src/lib/assets/assets.ts | 29 +++- 7 files changed, 601 insertions(+), 1 deletion(-) create mode 100644 src/core/assets/animation-mask-utils.ts create mode 100644 src/core/assets/animation-mask.ts create mode 100644 src/core/assets/test/animation-mask.test.ts diff --git a/src/api/assets/assets.ts b/src/api/assets/assets.ts index 2163792fb..0e4beb7dc 100644 --- a/src/api/assets/assets.ts +++ b/src/api/assets/assets.ts @@ -76,6 +76,12 @@ import { TUUIDOrPath, TUrlOrUUID, TUrlOrPath, + SchemaAnimationMaskDump, + SchemaAnimationMaskChanges, + SchemaVoidResult, + TAnimationMaskDump, + TAnimationMaskChanges, + TVoidResult, } from './schema'; import { z } from 'zod'; import { description, param, result, title, tool } from '../decorator/decorator.js'; @@ -83,6 +89,13 @@ import { COMMON_STATUS, CommonResultType, getCommonErrorStatus, HttpStatusCode } import { assetDBManager, assetManager } from '../../core/assets'; import { IAssetInfo } from '../../core/assets/@types/public'; import { SchemaUrlOrPath, SchemaUrlOrUUID, SchemaUUIDOrPath } from '../base/schema-identifier'; +import { + changeAnimationMaskDump as changeAnimationMaskDumpCore, + clearAnimationMaskNodes as clearAnimationMaskNodesCore, + importAnimationMaskSkeleton as importAnimationMaskSkeletonCore, + queryAnimationMask as queryAnimationMaskCore, + saveAnimationMask as saveAnimationMaskCore, +} from '../../core/assets/animation-mask'; export class AssetsApi { @@ -408,6 +421,117 @@ export class AssetsApi { return ret; } + @tool('assets-animation-mask-query') + @title('Query Animation Mask') + @description('Query a .animask AnimationMask asset and return a stable DTO containing joint paths, enabled states, and tree structure. This tool does not expose Creator inspector reflection dump.') + @result(SchemaAnimationMaskDump) + async queryAnimationMask(@param(SchemaUrlOrUUIDOrPath) uuid: TUrlOrUUIDOrPath): Promise> { + const ret: CommonResultType = { + code: COMMON_STATUS.SUCCESS, + data: null, + }; + + try { + ret.data = await queryAnimationMaskCore(uuid); + } catch (e) { + ret.code = getCommonErrorStatus(e); + console.error('query animation mask fail:', e instanceof Error ? e.message : String(e)); + ret.reason = e instanceof Error ? e.message : String(e); + } + + return ret; + } + + @tool('assets-animation-mask-import-skeleton') + @title('Import Animation Mask Skeleton') + @description('Import joint paths from a Prefab or glTF-scene asset into an AnimationMask. Existing joint states are preserved and missing paths are appended as enabled. Pass the glTF-scene sub-asset UUID when possible.') + @result(SchemaAnimationMaskDump) + async importAnimationMaskSkeleton( + @param(SchemaUrlOrUUIDOrPath) uuid: TUrlOrUUIDOrPath, + @param(SchemaUrlOrUUIDOrPath) skeletonSourceUuid: TUrlOrUUIDOrPath + ): Promise> { + const ret: CommonResultType = { + code: COMMON_STATUS.SUCCESS, + data: null, + }; + + try { + ret.data = await importAnimationMaskSkeletonCore(uuid, skeletonSourceUuid); + } catch (e) { + ret.code = getCommonErrorStatus(e); + console.error('import animation mask skeleton fail:', e instanceof Error ? e.message : String(e)); + ret.reason = e instanceof Error ? e.message : String(e); + } + + return ret; + } + + @tool('assets-animation-mask-clear-nodes') + @title('Clear Animation Mask Nodes') + @description('Clear all joint paths from an AnimationMask asset and return the updated stable DTO.') + @result(SchemaAnimationMaskDump) + async clearAnimationMaskNodes(@param(SchemaUrlOrUUIDOrPath) uuid: TUrlOrUUIDOrPath): Promise> { + const ret: CommonResultType = { + code: COMMON_STATUS.SUCCESS, + data: null, + }; + + try { + ret.data = await clearAnimationMaskNodesCore(uuid); + } catch (e) { + ret.code = getCommonErrorStatus(e); + console.error('clear animation mask nodes fail:', e instanceof Error ? e.message : String(e)); + ret.reason = e instanceof Error ? e.message : String(e); + } + + return ret; + } + + @tool('assets-animation-mask-change-dump') + @title('Change Animation Mask Dump') + @description('Apply path-based changes to an AnimationMask stable DTO. recursive defaults to false; pass recursive=true to update descendant paths.') + @result(SchemaAnimationMaskDump) + async changeAnimationMaskDump( + @param(SchemaUrlOrUUIDOrPath) uuid: TUrlOrUUIDOrPath, + @param(SchemaAnimationMaskChanges) changes: TAnimationMaskChanges + ): Promise> { + const ret: CommonResultType = { + code: COMMON_STATUS.SUCCESS, + data: null, + }; + + try { + ret.data = await changeAnimationMaskDumpCore(uuid, changes); + } catch (e) { + ret.code = getCommonErrorStatus(e); + console.error('change animation mask dump fail:', e instanceof Error ? e.message : String(e)); + ret.reason = e instanceof Error ? e.message : String(e); + } + + return ret; + } + + @tool('assets-animation-mask-save') + @title('Save Animation Mask') + @description('Normalize and save the current AnimationMask asset content, then reimport the asset.') + @result(SchemaVoidResult) + async saveAnimationMask(@param(SchemaUrlOrUUIDOrPath) uuid: TUrlOrUUIDOrPath): Promise> { + const ret: CommonResultType = { + code: COMMON_STATUS.SUCCESS, + data: null, + }; + + try { + await saveAnimationMaskCore(uuid); + } catch (e) { + ret.code = getCommonErrorStatus(e); + console.error('save animation mask fail:', e instanceof Error ? e.message : String(e)); + ret.reason = e instanceof Error ? e.message : String(e); + } + + return ret; + } + /** * Query Asset UUID // 查询资源 UUID */ diff --git a/src/api/assets/schema.ts b/src/api/assets/schema.ts index d2ffcddee..dce478772 100644 --- a/src/api/assets/schema.ts +++ b/src/api/assets/schema.ts @@ -175,6 +175,22 @@ export const SchemaRefreshDirResult = z.null().describe('Refresh asset directory export const SchemaUUIDResult = z.string().nullable().describe('Unique identifier UUID of the asset'); // 资源的唯一标识符 UUID export const SchemaPathResult = z.string().nullable().describe('File system path of the asset'); // 资源的文件系统路径 export const SchemaUrlResult = z.string().nullable().describe('Database URL address of the asset'); // 资源的数据库 URL 地址 +export const SchemaAnimationMaskJoint: z.ZodType = z.lazy(() => z.object({ + path: z.string().min(1).describe('Joint path relative to the skeleton root, for example "spine/leftArm"'), // 相对骨骼根节点的关节路径 + enabled: z.boolean().describe('Whether this joint is enabled in the animation mask'), // 关节是否启用 + children: z.array(SchemaAnimationMaskJoint).optional().describe('Child joint masks'), // 子关节遮罩 +})); +export const SchemaAnimationMaskDump = z.object({ + version: z.literal(1).describe('Animation mask DTO schema version'), // DTO 版本 + assetUuid: z.string().min(1).describe('AnimationMask asset UUID'), // AnimationMask 资源 UUID + joints: z.array(SchemaAnimationMaskJoint).describe('Animation mask joint tree'), // 关节遮罩树 +}).describe('Stable AnimationMask DTO for panel and CLI consumption'); // 面板/CLI 专用稳定 AnimationMask DTO +export const SchemaAnimationMaskChanges = z.array(z.object({ + path: z.string().min(1).describe('Joint path to update'), // 要更新的关节路径 + enabled: z.boolean().describe('New enabled state'), // 新的启用状态 + recursive: z.boolean().optional().describe('Whether to apply the change to descendant joint paths. Defaults to false.'), // 是否递归更新子树 +})).describe('AnimationMask path patch list'); // AnimationMask 路径 patch 列表 +export const SchemaVoidResult = z.null().describe('No result data'); // 无返回数据 // Asset operation related // 资源操作相关 export const SchemaQueryAssetType = z.enum(['asset', 'script', 'all']).describe('Query asset type: asset (normal asset), script (script), all (all)'); // 查询资源类型:asset(普通资源)、script(脚本)、all(全部) @@ -241,6 +257,9 @@ export type TRefreshDirResult = z.infer; export type TUUIDResult = z.infer; export type TPathResult = z.infer; export type TUrlResult = z.infer; +export type TAnimationMaskDump = z.infer; +export type TAnimationMaskChanges = z.infer; +export type TVoidResult = z.infer; export type TQueryAssetType = z.infer; export type TFilterPluginOptions = z.infer; export type TPluginScriptInfo = z.infer; diff --git a/src/core/assets/@types/public.d.ts b/src/core/assets/@types/public.d.ts index 0f51de2af..9c7ed8280 100644 --- a/src/core/assets/@types/public.d.ts +++ b/src/core/assets/@types/public.d.ts @@ -72,6 +72,24 @@ export interface DeleteAssetOptions { useTrash?: boolean; } +export interface AnimationMaskDump { + version: 1; + assetUuid: string; + joints: AnimationMaskJoint[]; +} + +export interface AnimationMaskJoint { + path: string; + enabled: boolean; + children?: AnimationMaskJoint[]; +} + +export interface AnimationMaskChange { + path: string; + enabled: boolean; + recursive?: boolean; +} + // Basic information about the resource // 资源的基础信息 export interface AssetInfo extends IAssetInfo { diff --git a/src/core/assets/animation-mask-utils.ts b/src/core/assets/animation-mask-utils.ts new file mode 100644 index 000000000..d5c802eac --- /dev/null +++ b/src/core/assets/animation-mask-utils.ts @@ -0,0 +1,168 @@ +import type { AnimationMaskChange, AnimationMaskDump, AnimationMaskJoint } from './@types/public'; + +export const ANIMATION_MASK_TYPE = 'cc.animation.AnimationMask'; +export const JOINT_MASK_TYPE = 'cc.JointMask'; +export const PREFAB_TYPE = 'cc.Prefab'; +export const NODE_TYPE = 'cc.Node'; + +export interface SerializedJointMask { + __type__?: string; + path: string; + enabled: boolean; +} + +export type SerializedNodeRef = { __id__: number }; + +export interface SerializedNode { + __type__?: string; + _name?: string; + _children?: SerializedNodeRef[]; +} + +export function assertRecord(value: unknown, message: string): asserts value is Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error(message); + } +} + +export function normalizeJointPath(path: string): string { + return path.split('/').map((segment) => segment.trim()).filter(Boolean).join('/'); +} + +export function normalizeJointMasks(value: unknown): SerializedJointMask[] { + if (value === undefined) { + return []; + } + if (!Array.isArray(value)) { + throw new Error('AnimationMask _jointMasks must be an array'); + } + + const result: SerializedJointMask[] = []; + const seen = new Set(); + for (const item of value) { + assertRecord(item, 'AnimationMask joint mask item must be an object'); + const path = normalizeJointPath(String(item.path ?? '')); + if (!path) { + continue; + } + if (seen.has(path)) { + throw new Error(`Duplicate AnimationMask joint path: ${path}`); + } + seen.add(path); + result.push({ + __type__: typeof item.__type__ === 'string' ? item.__type__ : JOINT_MASK_TYPE, + path, + enabled: item.enabled !== false, + }); + } + return result; +} + +export function jointMasksToDump(assetUuid: string, jointMasks: SerializedJointMask[]): AnimationMaskDump { + const nodeMap = new Map(); + for (const mask of jointMasks) { + nodeMap.set(mask.path, { + path: mask.path, + enabled: mask.enabled, + }); + } + + const roots: AnimationMaskJoint[] = []; + const sorted = Array.from(nodeMap.values()).sort((a, b) => a.path.localeCompare(b.path)); + for (const node of sorted) { + const parentPath = findNearestExplicitParentPath(node.path, nodeMap); + if (!parentPath) { + roots.push(node); + continue; + } + const parent = nodeMap.get(parentPath)!; + parent.children = parent.children || []; + parent.children.push(node); + } + + return { + version: 1, + assetUuid, + joints: roots, + }; +} + +function findNearestExplicitParentPath(path: string, nodeMap: Map): string | null { + let index = path.lastIndexOf('/'); + while (index > 0) { + const parentPath = path.slice(0, index); + if (nodeMap.has(parentPath)) { + return parentPath; + } + index = parentPath.lastIndexOf('/'); + } + return null; +} + +function isNodeRef(value: unknown): value is SerializedNodeRef { + return typeof value === 'object' + && value !== null + && !Array.isArray(value) + && typeof (value as SerializedNodeRef).__id__ === 'number'; +} + +export function extractPrefabJointPaths(prefabJSON: unknown[]): string[] { + const root = prefabJSON[1]; + assertRecord(root, 'Prefab JSON must contain root node at index 1'); + if (root.__type__ !== NODE_TYPE) { + throw new Error('Prefab JSON root entry must be cc.Node'); + } + + const paths: string[] = []; + const seen = new Set(); + visitChildren(root as SerializedNode, ''); + return paths; + + function visitChildren(node: SerializedNode, parentPath: string): void { + const children = Array.isArray(node._children) ? node._children : []; + for (const childRef of children) { + if (!isNodeRef(childRef)) { + continue; + } + const child = prefabJSON[childRef.__id__] as SerializedNode | undefined; + if (!child || child.__type__ !== NODE_TYPE) { + continue; + } + const name = String(child._name ?? '').trim(); + if (!name) { + continue; + } + const path = parentPath ? `${parentPath}/${name}` : name; + if (seen.has(path)) { + throw new Error(`Duplicate skeleton joint path: ${path}`); + } + seen.add(path); + paths.push(path); + visitChildren(child, path); + } + } +} + +export function applyJointChanges(jointMasks: SerializedJointMask[], changes: AnimationMaskChange[]): SerializedJointMask[] { + const result = jointMasks.map((joint) => ({ ...joint })); + const pathToJoint = new Map(result.map((joint) => [joint.path, joint])); + + for (const change of changes) { + const path = normalizeJointPath(change.path); + if (!path) { + throw new Error('AnimationMask change path must not be empty'); + } + const target = pathToJoint.get(path); + if (!target) { + throw new Error(`AnimationMask joint path not found: ${path}`); + } + const shouldUpdate = (joint: SerializedJointMask) => joint.path === path || (!!change.recursive && joint.path.startsWith(`${path}/`)); + for (const joint of result) { + if (shouldUpdate(joint)) { + joint.enabled = change.enabled; + } + } + } + + return result; +} diff --git a/src/core/assets/animation-mask.ts b/src/core/assets/animation-mask.ts new file mode 100644 index 000000000..619b54719 --- /dev/null +++ b/src/core/assets/animation-mask.ts @@ -0,0 +1,164 @@ +import { Asset } from '@cocos/asset-db'; +import { readJSON } from 'fs-extra'; + +import assetManager from './manager/asset'; +import type { IAsset } from './@types/protected'; +import type { AnimationMaskChange, AnimationMaskDump } from './@types/public'; +import { + ANIMATION_MASK_TYPE, + JOINT_MASK_TYPE, + PREFAB_TYPE, + assertRecord, + applyJointChanges, + extractPrefabJointPaths, + jointMasksToDump, + normalizeJointMasks, + type SerializedJointMask, +} from './animation-mask-utils'; + +const ANIMATION_MASK_SERIALIZED_TYPE = 'cc.animation.AnimationMask'; +const ANIMATION_MASK_ASSET_TYPE = 'cc.AnimationMask'; + +interface SerializedAnimationMask { + __type__?: string; + _jointMasks?: SerializedJointMask[]; + [key: string]: unknown; +} + +function ensureAnimationMaskAsset(asset: IAsset | null, id: string): IAsset { + if (!asset) { + throw new Error(`AnimationMask asset not found: ${id}`); + } + const type = assetManager.queryAssetProperty(asset, 'type'); + if (type !== ANIMATION_MASK_ASSET_TYPE && type !== ANIMATION_MASK_TYPE) { + throw new Error(`Asset is not an AnimationMask: ${id}`); + } + if (!(asset instanceof Asset) || !asset.source) { + throw new Error(`AnimationMask asset must be a source asset: ${id}`); + } + return asset; +} + +function ensureReadonly(value: unknown): never { + throw new Error(`Unsupported AnimationMask source content: ${String(value)}`); +} + +function getLibraryJSONPath(asset: IAsset): string { + if (!asset.library) { + throw new Error(`Asset has no imported library JSON: ${asset.uuid}`); + } + return `${asset.library}.json`; +} + +function resolveSkeletonSourceAsset(id: string): IAsset { + const asset = assetManager.queryAsset(id); + if (!asset) { + throw new Error(`Skeleton source asset not found: ${id}`); + } + + const importer = asset.meta?.importer; + if (importer === 'gltf' || importer === 'fbx') { + const gltfScenes = Object.values(asset.subAssets || {}) + .filter((subAsset) => subAsset.meta?.importer === 'gltf-scene'); + if (!gltfScenes.length) { + throw new Error(`glTF source has no gltf-scene sub asset: ${id}`); + } + return gltfScenes[0] as IAsset; + } + + if (importer !== 'prefab' && importer !== 'gltf-scene') { + throw new Error(`Skeleton source must be a Prefab or glTF scene: ${id}`); + } + + const type = assetManager.queryAssetProperty(asset, 'type'); + if (type !== PREFAB_TYPE) { + throw new Error(`Skeleton source is not a Prefab asset: ${id}`); + } + + return asset; +} + +async function readAnimationMaskSource(asset: IAsset): Promise { + const source = (asset as Asset).source; + if (!source) { + return ensureReadonly(asset.uuid); + } + const data = await readJSON(source); + assertRecord(data, `Invalid AnimationMask JSON: ${source}`); + if (data.__type__ !== ANIMATION_MASK_SERIALIZED_TYPE) { + throw new Error(`Invalid AnimationMask type: ${String(data.__type__)}`); + } + return data as SerializedAnimationMask; +} + +async function writeAnimationMaskSource(asset: IAsset, data: SerializedAnimationMask): Promise { + const source = (asset as Asset).source; + if (!source) { + return ensureReadonly(asset.uuid); + } + await assetManager.saveAsset(asset.uuid, JSON.stringify(data, undefined, 2)); +} + +async function readPrefabJSON(asset: IAsset): Promise { + const file = asset instanceof Asset ? asset.source : getLibraryJSONPath(asset); + const data = await readJSON(file); + if (!Array.isArray(data)) { + throw new Error(`Invalid Prefab JSON: ${file}`); + } + if (!data[0] || (data[0] as { __type__?: string }).__type__ !== PREFAB_TYPE) { + throw new Error(`Invalid Prefab asset type: ${file}`); + } + return data; +} + +export async function queryAnimationMask(uuid: string): Promise { + const asset = ensureAnimationMaskAsset(assetManager.queryAsset(uuid), uuid); + const data = await readAnimationMaskSource(asset); + return jointMasksToDump(asset.uuid, normalizeJointMasks(data._jointMasks)); +} + +export async function importAnimationMaskSkeleton(uuid: string, skeletonSourceUuid: string): Promise { + const maskAsset = ensureAnimationMaskAsset(assetManager.queryAsset(uuid), uuid); + const sourceAsset = resolveSkeletonSourceAsset(skeletonSourceUuid); + const maskData = await readAnimationMaskSource(maskAsset); + const currentMasks = normalizeJointMasks(maskData._jointMasks); + const currentPathSet = new Set(currentMasks.map((joint) => joint.path)); + const importedPaths = extractPrefabJointPaths(await readPrefabJSON(sourceAsset)); + + for (const path of importedPaths) { + if (!currentPathSet.has(path)) { + currentMasks.push({ __type__: JOINT_MASK_TYPE, path, enabled: true }); + currentPathSet.add(path); + } + } + + maskData._jointMasks = currentMasks; + await writeAnimationMaskSource(maskAsset, maskData); + return jointMasksToDump(maskAsset.uuid, currentMasks); +} + +export async function clearAnimationMaskNodes(uuid: string): Promise { + const asset = ensureAnimationMaskAsset(assetManager.queryAsset(uuid), uuid); + const data = await readAnimationMaskSource(asset); + data._jointMasks = []; + await writeAnimationMaskSource(asset, data); + return jointMasksToDump(asset.uuid, []); +} + +export async function changeAnimationMaskDump(uuid: string, changes: AnimationMaskChange[]): Promise { + const asset = ensureAnimationMaskAsset(assetManager.queryAsset(uuid), uuid); + const data = await readAnimationMaskSource(asset); + const nextMasks = applyJointChanges(normalizeJointMasks(data._jointMasks), changes); + data._jointMasks = nextMasks; + await writeAnimationMaskSource(asset, data); + return jointMasksToDump(asset.uuid, nextMasks); +} + +export async function saveAnimationMask(uuid: string): Promise { + const asset = ensureAnimationMaskAsset(assetManager.queryAsset(uuid), uuid); + const data = await readAnimationMaskSource(asset); + await writeAnimationMaskSource(asset, { + ...data, + _jointMasks: normalizeJointMasks(data._jointMasks), + }); +} diff --git a/src/core/assets/test/animation-mask.test.ts b/src/core/assets/test/animation-mask.test.ts new file mode 100644 index 000000000..954f0d6cc --- /dev/null +++ b/src/core/assets/test/animation-mask.test.ts @@ -0,0 +1,80 @@ +import { + applyJointChanges, + extractPrefabJointPaths, + jointMasksToDump, + normalizeJointMasks, +} from '../animation-mask-utils'; + +describe('animation mask helpers', () => { + test('extractPrefabJointPaths skips the prefab root and keeps Creator path semantics', () => { + const prefab = [ + { __type__: 'cc.Prefab' }, + { __type__: 'cc.Node', _name: 'Root', _children: [{ __id__: 2 }, { __id__: 5 }] }, + { __type__: 'cc.Node', _name: 'Hips', _children: [{ __id__: 3 }, { __id__: 4 }] }, + { __type__: 'cc.Node', _name: 'Spine', _children: [] }, + { __type__: 'cc.Node', _name: 'Leg', _children: [] }, + { __type__: 'cc.Node', _name: 'Arm', _children: [] }, + ]; + + expect(extractPrefabJointPaths(prefab)).toEqual([ + 'Hips', + 'Hips/Spine', + 'Hips/Leg', + 'Arm', + ]); + }); + + test('extractPrefabJointPaths rejects duplicate paths', () => { + const prefab = [ + { __type__: 'cc.Prefab' }, + { __type__: 'cc.Node', _name: 'Root', _children: [{ __id__: 2 }, { __id__: 3 }] }, + { __type__: 'cc.Node', _name: 'Bone', _children: [] }, + { __type__: 'cc.Node', _name: 'Bone', _children: [] }, + ]; + + expect(() => extractPrefabJointPaths(prefab)).toThrow('Duplicate skeleton joint path: Bone'); + }); + + test('jointMasksToDump builds a tree from explicit parent paths only', () => { + const dump = jointMasksToDump('mask-uuid', normalizeJointMasks([ + { path: 'Root/Child', enabled: false }, + { path: 'Root', enabled: true }, + { path: 'Orphan/Leaf', enabled: true }, + ])); + + expect(dump).toEqual({ + version: 1, + assetUuid: 'mask-uuid', + joints: [ + { path: 'Orphan/Leaf', enabled: true }, + { + path: 'Root', + enabled: true, + children: [ + { path: 'Root/Child', enabled: false }, + ], + }, + ], + }); + }); + + test('applyJointChanges defaults to non-recursive and supports explicit recursive updates', () => { + const masks = normalizeJointMasks([ + { path: 'Root', enabled: true }, + { path: 'Root/Child', enabled: true }, + { path: 'Other', enabled: true }, + ]); + + expect(applyJointChanges(masks, [{ path: 'Root', enabled: false }])).toEqual([ + { __type__: 'cc.JointMask', path: 'Root', enabled: false }, + { __type__: 'cc.JointMask', path: 'Root/Child', enabled: true }, + { __type__: 'cc.JointMask', path: 'Other', enabled: true }, + ]); + + expect(applyJointChanges(masks, [{ path: 'Root', enabled: false, recursive: true }])).toEqual([ + { __type__: 'cc.JointMask', path: 'Root', enabled: false }, + { __type__: 'cc.JointMask', path: 'Root/Child', enabled: false }, + { __type__: 'cc.JointMask', path: 'Other', enabled: true }, + ]); + }); +}); diff --git a/src/lib/assets/assets.ts b/src/lib/assets/assets.ts index 96051b42c..2c3dd8e6b 100644 --- a/src/lib/assets/assets.ts +++ b/src/lib/assets/assets.ts @@ -1,4 +1,4 @@ -import type { AssetOperationOption, CreateAssetByTypeOptions, DeleteAssetOptions, IAssetFileSystemProvider, IAssetInfo, IAssetMeta, ISupportCreateType, QueryAssetsOption } from '../../core/assets/@types/public'; +import type { AnimationMaskChange, AnimationMaskDump, AssetOperationOption, CreateAssetByTypeOptions, DeleteAssetOptions, IAssetFileSystemProvider, IAssetInfo, IAssetMeta, ISupportCreateType, QueryAssetsOption } from '../../core/assets/@types/public'; import type { CreateAssetOptions, IAssetConfig, IAssetDBInfo, ICreateMenuInfo, IUerDataConfigItem, QueryAssetType, ThumbnailInfo, ThumbnailSize } from '../../core/assets/@types/protected'; import type { FilterPluginOptions, IPluginScriptInfo } from '../../core/scripting/interface'; import { assetDBManager, assetManager } from '../../core/assets'; @@ -185,6 +185,33 @@ export async function saveAsset( return await assetManager.saveAsset(pathOrUrlOrUUID, data); } +export const animationMask = { + async query(uuid: string): Promise { + const { queryAnimationMask } = await import('../../core/assets/animation-mask'); + return queryAnimationMask(uuid); + }, + + async importSkeleton(uuid: string, skeletonSourceUuid: string): Promise { + const { importAnimationMaskSkeleton } = await import('../../core/assets/animation-mask'); + return importAnimationMaskSkeleton(uuid, skeletonSourceUuid); + }, + + async clearNodes(uuid: string): Promise { + const { clearAnimationMaskNodes } = await import('../../core/assets/animation-mask'); + return clearAnimationMaskNodes(uuid); + }, + + async changeDump(uuid: string, changes: AnimationMaskChange[]): Promise { + const { changeAnimationMaskDump } = await import('../../core/assets/animation-mask'); + return changeAnimationMaskDump(uuid, changes); + }, + + async save(uuid: string): Promise { + const { saveAnimationMask } = await import('../../core/assets/animation-mask'); + return saveAnimationMask(uuid); + }, +}; + /** * Query Asset UUID // 查询资源 UUID */