diff --git a/packages/cocos-cli-types/__tests__/cli.test.ts b/packages/cocos-cli-types/__tests__/cli.test.ts index 00724e272..22eb3d52f 100644 --- a/packages/cocos-cli-types/__tests__/cli.test.ts +++ b/packages/cocos-cli-types/__tests__/cli.test.ts @@ -30,13 +30,13 @@ describe('cocos-cli-types: cli', () => { it('IServiceManager.Node should have CRUD methods', () => { type NodeKeys = keyof IServiceManager['Node']; - const nodeMethods: NodeKeys[] = ['createNodeByType', 'createNodeByAsset', 'deleteNode', 'updateNode', 'queryNode', 'queryNodeTree']; + const nodeMethods: NodeKeys[] = ['createByType', 'createByAsset', 'delete', 'update', 'query', 'queryNodeTree']; expect(nodeMethods.length).toBeGreaterThan(0); }); it('IServiceManager.Component should have component methods', () => { type CompKeys = keyof IServiceManager['Component']; - const compMethods: CompKeys[] = ['addComponent', 'removeComponent', 'setProperty', 'queryComponent', 'queryAllComponent']; + const compMethods: CompKeys[] = ['add', 'remove', 'setProperty', 'query', 'queryAll']; expect(compMethods.length).toBeGreaterThan(0); }); diff --git a/packages/cocos-cli-types/package.json b/packages/cocos-cli-types/package.json index f88e920cf..1f8e3ecc0 100644 --- a/packages/cocos-cli-types/package.json +++ b/packages/cocos-cli-types/package.json @@ -2,7 +2,7 @@ "name": "@cocos/cocos-cli-types", "description": "types for cocos cli", "author": "cocos cli", - "version": "0.0.1-alpha.24.3", + "version": "0.0.1-alpha.24.4", "main": "index.d.ts", "types": "index.d.ts", "exports": { diff --git a/src/api/scene/component-schema.ts b/src/api/scene/component-schema.ts index 0c805b6c0..330161e74 100644 --- a/src/api/scene/component-schema.ts +++ b/src/api/scene/component-schema.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import type { IComponent } from '../../core/scene'; +import type { IComponentInfo } from '../../core/scene'; import { SchemaComponentIdentifier } from '../base/schema-identifier'; import { SchemaCompPrefabInfo } from './prefab-info-schema'; @@ -109,7 +109,7 @@ export const SchemaSetPropertyOptions = z.object({ ) }).describe('Information required to set component properties'); // 设置组件属性所需要的信息 -export const SchemaComponent: z.ZodType = SchemaComponentIdentifier.extend({ +export const SchemaComponent: z.ZodType = SchemaComponentIdentifier.extend({ properties: z.record( z.string().describe('Property name'), // 属性名称 SchemaProperty, diff --git a/src/api/scene/component.ts b/src/api/scene/component.ts index b16e9fcba..5f082ccff 100644 --- a/src/api/scene/component.ts +++ b/src/api/scene/component.ts @@ -17,7 +17,8 @@ import { import { description, param, result, title, tool } from '../decorator/decorator.js'; import { COMMON_STATUS, CommonResultType } from '../base/schema-base'; -import { Scene, ISetPropertyOptions, IComponent } from '../../core/scene'; +import { Scene, IComponentInfo } from '../../core/scene'; +import { ISetPropertyOptionsInfo } from '../../core/scene/common/cli/component'; export class ComponentApi { @@ -30,7 +31,7 @@ export class ComponentApi { @result(SchemaComponentResult) async addComponent(@param(SchemaAddComponentInfo) addComponentInfo: TAddComponentInfo): Promise> { try { - const component = await Scene.addComponent({ nodePathOrUuid: addComponentInfo.nodePath, component: addComponentInfo.component }); + const component = await Scene.Component.add({ nodePath: addComponentInfo.nodePath, component: addComponentInfo.component }); return { code: COMMON_STATUS.SUCCESS, data: component @@ -52,7 +53,7 @@ export class ComponentApi { @result(SchemaBooleanResult) async removeComponent(@param(SchemaRemoveComponent) component: TRemoveComponentOptions): Promise> { try { - const result = await Scene.removeComponent(component); + const result = await Scene.Component.remove(component); return { code: COMMON_STATUS.SUCCESS, data: result @@ -74,13 +75,13 @@ export class ComponentApi { @result(SchemaComponentResult) async queryComponent(@param(SchemaQueryComponent) component: TQueryComponentOptions): Promise> { try { - const componentInfo = await Scene.queryComponent(component); + const componentInfo = await Scene.Component.query(component); if (!componentInfo) { throw new Error(`component not found: ${component.path}`); } return { code: COMMON_STATUS.SUCCESS, - data: componentInfo as IComponent + data: componentInfo as IComponentInfo }; } catch (e) { return { @@ -99,7 +100,7 @@ export class ComponentApi { @result(SchemaBooleanResult) async setProperty(@param(SchemaSetPropertyOptions) setPropertyOptions?: TSetPropertyOptions): Promise> { try { - const result = await Scene.setProperty(setPropertyOptions as ISetPropertyOptions); + const result = await Scene.Component.setProperty(setPropertyOptions as ISetPropertyOptionsInfo); return { code: COMMON_STATUS.SUCCESS, data: result @@ -121,7 +122,7 @@ export class ComponentApi { @result(SchemaQueryAllComponentResult) async queryAllComponent(): Promise> { try { - const components = await Scene.queryAllComponent(); + const components = await Scene.Component.queryAll(); return { code: COMMON_STATUS.SUCCESS, data: components, diff --git a/src/api/scene/node-schema.ts b/src/api/scene/node-schema.ts index 5507194cf..398fdc3a6 100644 --- a/src/api/scene/node-schema.ts +++ b/src/api/scene/node-schema.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { NodeType } from '../../core/scene'; -import { INode } from '../../core/scene'; +import { INodeInfo } from '../../core/scene'; import { SchemaQuat, SchemaVec3 } from '../base/schema-value-types'; import { SchemaNodeIdentifier, SchemaComponentIdentifier } from '../base/schema-identifier'; import { SchemaPrefabInfo } from './prefab-info-schema'; @@ -34,7 +34,7 @@ export const SchemaComponentOrDetail = z.union([ SchemaComponentIdentifier ]).describe('components on the node'); // 节点上的组件信息 -export const SchemaNode: z.ZodType = SchemaNodeIdentifier.extend({ +export const SchemaNode: z.ZodType = SchemaNodeIdentifier.extend({ properties: SchemaNodeProperty.describe('Node properties'), // 节点属性 prefab: z.union([SchemaPrefabInfo, z.null()]).describe('Prefab information'), // 预制体信息 children: z.array(z.lazy(() => SchemaNode)).optional().describe('List of child nodes'), // 子节点列表 @@ -55,7 +55,7 @@ export const SchemaNodeQuery = z.object({ }).describe('To configure options for node query, the Scene must be open first. The result is the intersection of the passed information'); // 查询节点的选项参数,查询结果是传入的信息的交集 // 查询节点的结果 -export const SchemaNodeQueryResult: z.ZodType = SchemaNode; +export const SchemaNodeQueryResult: z.ZodType = SchemaNode; //节点更新的参数 export const SchemaNodeUpdate = z.object({ diff --git a/src/api/scene/node.ts b/src/api/scene/node.ts index 5dbcef6d4..97437d852 100644 --- a/src/api/scene/node.ts +++ b/src/api/scene/node.ts @@ -18,7 +18,7 @@ import { } from './node-schema'; import { description, param, result, title, tool } from '../decorator/decorator.js'; import { COMMON_STATUS, CommonResultType } from '../base/schema-base'; -import { ICreateByNodeTypeParams, Scene } from '../../core/scene'; +import { ICreateByNodeTypeParams, INodeInfo, Scene } from '../../core/scene'; export class NodeApi { @@ -35,7 +35,7 @@ export class NodeApi { data: undefined, }; try { - const resultNode = await Scene.createNodeByType(options as ICreateByNodeTypeParams); + const resultNode = await Scene.Node.createByType(options as ICreateByNodeTypeParams); if (resultNode) { ret.data = resultNode; } @@ -62,7 +62,7 @@ export class NodeApi { data: undefined, }; try { - const resultNode = await Scene.createNodeByAsset(options); + const resultNode = await Scene.Node.createByAsset(options); if (resultNode) { ret.data = resultNode; } @@ -90,7 +90,7 @@ export class NodeApi { }; try { - const result = await Scene.deleteNode(options); + const result = await Scene.Node.delete(options); if (!result) throw new Error(`node not found at path: ${options.path}`); ret.data = { path: result.path, @@ -114,7 +114,7 @@ export class NodeApi { @result(SchemaNodeUpdateResult) async updateNode(@param(SchemaNodeUpdate) options: TUpdateNodeOptions): Promise> { try { - const data = await Scene.updateNode(options); + const data = await Scene.Node.update(options); return { data: data, code: COMMON_STATUS.SUCCESS, @@ -142,7 +142,7 @@ export class NodeApi { }; try { - const result = await Scene.queryNode(options); + const result = await Scene.Node.query(options) as INodeInfo | null; if (!result) throw new Error(`node not found at path: ${options.path}`); ret.data = result; } catch (e) { diff --git a/src/core/base/i18n.ts b/src/core/base/i18n.ts index dac29ea21..2051783c7 100644 --- a/src/core/base/i18n.ts +++ b/src/core/base/i18n.ts @@ -33,8 +33,8 @@ class I18n { return i18nextInstance.t(key, obj); } /** - * 翻译 title - * @param title 原始 title 或者带有 i18n 开头的 title + * 翻译 name + * @param name 原始 name 或者带有 i18n 开头的 name */ transI18nName(name: string): string { if (!name || typeof name !== 'string') { @@ -49,12 +49,21 @@ class I18n { return name; } if (!i18nextInstance.exists(key)) { - console.debug(`${name} is not defined in i18n`); + // 引擎大部分都没翻译,这样会导致每次调用获取节点信息会答应大量的debug信息,这里暂时去掉。 + // console.debug(`${name} is not defined in i18n`); return name; } return i18nextInstance.t(key) || name; } + batchTransI18nName(names: string[]): Record { + const result: Record = {}; + for (const name of names) { + result[name] = this.transI18nName(name); + } + return result; + } + /** * 动态注册语言包的补丁内容 * @param language 语言代码,例如 zh、en diff --git a/src/core/scene/@types/public.d.ts b/src/core/scene/@types/public.d.ts index 8e8952de0..0bedd8eae 100644 --- a/src/core/scene/@types/public.d.ts +++ b/src/core/scene/@types/public.d.ts @@ -75,7 +75,7 @@ export interface IProperty { elementTypeData?: IProperty; // 数组里的数据的默认值 dump - path?: string; // 数据的搜索路径,这个是由使用方填充的 + path: string; // 数据的搜索路径 isArray?: boolean; invalid?: boolean; diff --git a/src/core/scene/common/cli/component.ts b/src/core/scene/common/cli/component.ts new file mode 100644 index 000000000..fbcfe6353 --- /dev/null +++ b/src/core/scene/common/cli/component.ts @@ -0,0 +1,27 @@ +import type { IPropertyValueType } from '../../@types/public'; +import type { ICompPrefabInfo } from '../prefab'; + +export interface IComponentIdentifier { + cid: string; + path: string; + uuid: string; + name: string; + type: string; + enabled: boolean; +} + +export interface IComponentInfo extends IComponentIdentifier { + properties: { [key: string]: IPropertyValueType }; + prefab: ICompPrefabInfo | null; +} + +/** + * CLI 设置组件属性的选项 + */ +export interface ISetPropertyOptionsInfo { + componentPath: string; + properties: { + [key: string]: null | undefined | number | boolean | string | object | Array; + }; + record?: boolean; +} diff --git a/src/core/scene/common/cli/index.ts b/src/core/scene/common/cli/index.ts new file mode 100644 index 000000000..d9bedee1a --- /dev/null +++ b/src/core/scene/common/cli/index.ts @@ -0,0 +1,4 @@ +export * from './node'; +export * from './component'; +export * from './scene'; +export * from './prefab'; diff --git a/src/core/scene/common/cli/node.ts b/src/core/scene/common/cli/node.ts new file mode 100644 index 000000000..0bda84893 --- /dev/null +++ b/src/core/scene/common/cli/node.ts @@ -0,0 +1,16 @@ +import type { INodeProperties } from '../node'; +import type { IComponentInfo, IComponentIdentifier } from './component'; +import type { IPrefabInfo } from './prefab'; + +export interface INodeIdentifier { + nodeId: string; + path: string; + name: string; +} + +export interface INodeInfo extends INodeIdentifier { + properties: INodeProperties; + components?: IComponentInfo[] | IComponentIdentifier[]; + children?: INodeInfo[]; + prefab: IPrefabInfo | null; +} diff --git a/src/core/scene/common/cli/prefab.ts b/src/core/scene/common/cli/prefab.ts new file mode 100644 index 000000000..2ffde4b92 --- /dev/null +++ b/src/core/scene/common/cli/prefab.ts @@ -0,0 +1,63 @@ +import type { INodeIdentifier } from './node'; +import type { IComponentIdentifier } from './component'; + +export enum OptimizationPolicy { + AUTO = 0, + SINGLE_INSTANCE = 1, + MULTI_INSTANCE = 2, +} + +export interface IPrefabInstance { + fileId: string; + prefabRootNode?: INodeIdentifier; + mountedChildren: IMountedChildrenInfo[]; + mountedComponents: IMountedComponentsInfo[]; + propertyOverrides: IPropertyOverrideInfo[]; + removedComponents: ITargetInfo[]; +} + +export interface IMountedChildrenInfo { + targetInfo: ITargetInfo | null; + nodes: INodeIdentifier[]; +} + +export interface IPropertyOverrideInfo { + targetInfo: ITargetInfo | null; + propertyPath: string[]; + value?: any; +} + +export interface ITargetInfo { + localID: string[]; +} + +export interface IMountedComponentsInfo { + targetInfo: ITargetInfo | null; + components: IComponentIdentifier[]; +} + +export interface ITargetOverrideDetail { + source: IComponentIdentifier | INodeIdentifier | null; + sourceInfo: ITargetInfo | null; + propertyPath: string[]; + target: INodeIdentifier | null; + targetInfo: ITargetInfo | null; +} + +export interface IPrefabDetail { + name: string; + uuid: string; + data: INodeIdentifier, + optimizationPolicy: OptimizationPolicy, + persistent: boolean, +} + +export interface IPrefabInfo { + /** 关联的预制体资源信息 */ + asset?: IPrefabDetail; + root?: INodeIdentifier; + instance?: IPrefabInstance; + fileId: string; + targetOverrides: ITargetOverrideDetail[]; + nestedPrefabInstanceRoots: INodeIdentifier[]; +} diff --git a/src/core/scene/common/cli/scene.ts b/src/core/scene/common/cli/scene.ts new file mode 100644 index 000000000..2176f4a15 --- /dev/null +++ b/src/core/scene/common/cli/scene.ts @@ -0,0 +1,11 @@ +import type { INodeInfo } from './node'; +import type { IComponentIdentifier } from './component'; +import type { IBaseIdentifier } from '../editor/base'; +import type { IPrefabInfo } from './prefab'; + +export interface ISceneInfo extends IBaseIdentifier { + name: string; + prefab: IPrefabInfo | null; + children: INodeInfo[]; + components: IComponentIdentifier[]; +} diff --git a/src/core/scene/common/component.ts b/src/core/scene/common/component.ts index 4ad283d01..8628aefb0 100644 --- a/src/core/scene/common/component.ts +++ b/src/core/scene/common/component.ts @@ -1,34 +1,13 @@ import type { Component, Node } from 'cc'; import type { IPropertyValueType, IProperty } from '../@types/public'; import type { IServiceEvents } from '../scene-process/service/core'; -import type { ICompPrefabInfo } from './prefab'; import type { IChangeNodeOptions, INodeEvents } from './node'; -/** - * 组件标识信息,包含组件的基本标识字段 - */ -export interface IComponentIdentifier { - cid: string; - path: string; - uuid: string; - name: string; - type: string; - enabled: boolean; -} - -/** - * CLI 使用的组件信息,属性值以扁平的 key-value 形式呈现 - */ -export interface IComponent extends IComponentIdentifier { - properties: { [key: string]: IPropertyValueType }; - prefab: ICompPrefabInfo | null; -} - /** * 编辑器使用的组件详细信息,属性值以 IProperty 编码形式呈现, * 包含 type、readonly、default 等元信息,用于编辑器 Inspector 面板渲染 */ -export interface IComponentForEditor extends IProperty { +export interface IComponent extends IProperty { value: { enabled: IPropertyValueType; uuid: IPropertyValueType; @@ -41,7 +20,7 @@ export interface IComponentForEditor extends IProperty { * 添加/创建组件的选项 */ export interface IAddComponentOptions { - nodePathOrUuid: string; + nodePath: string; component: string; } @@ -51,6 +30,10 @@ export interface IAddComponentOptions { export interface IRemoveComponentOptions { path: string; } +export interface IRemovedComponentInfo { + name: string; + fileID: string; +} /** * 查询组件的选项 @@ -59,23 +42,11 @@ export interface IQueryComponentOptions { path: string; } -/** - * CLI 设置组件属性的选项 - */ -export interface ISetPropertyOptions { - componentPath: string; // 修改属性的对象的 uuid - // key: string; // 属性的 key - properties: { - [key: string]: null | undefined | number | boolean | string | object | Array; - }; // 属性 dump 出来的数据 - record?: boolean;// 是否记录undo -} - /** * 编辑器设置组件属性的选项 */ -export interface ISetPropertyOptionsForEditor { - uuid: string; // 修改属性的对象的 uuid +export interface ISetPropertyOptions { + nodePath: string; // 修改属性的节点路径 path: string; // 属性挂载对象的搜索路径 // key: string; // 属性的 key dump: IProperty; // 属性 dump 出来的数据 @@ -86,7 +57,7 @@ export interface ISetPropertyOptionsForEditor { * 执行组件方法的选项 */ export interface IExecuteComponentMethodOptions { - uuid: string; + path: string; // 组件路径,如 'Canvas/cc.Label_1' name: string; args: any[]; } @@ -104,12 +75,12 @@ export interface IQueryClassesOptions { */ export interface IComponentEvents extends INodeEvents { 'component:add': [Component]; - 'component:before-remove': [Component]; 'component:remove': [Component]; 'component:set-property': [Component, IChangeNodeOptions]; 'component:added': [Component]; 'component:removed': [Component]; 'component:before-add-component': [string, Node]; + 'component:before-remove-component': [Component]; } /** @@ -117,8 +88,14 @@ export interface IComponentEvents extends INodeEvents { */ export interface IPublicComponentService extends Omit { } + 'unregisterCompMgrEvents' | + 'reset' | + 'queryClasses' | + 'queryFunctionOfNode' | + 'executeMethod' | + 'hasScript' +> { +} /** * 组件服务接口,定义了所有组件相关的操作方法 @@ -127,98 +104,81 @@ export interface IComponentService extends IServiceEvents { /** * 添加组件到指定节点,返回添加后的组件信息 * @param params - 添加组件选项 - * @param params.nodePathOrUuid - 目标节点路径或 UUID + * @param params.nodePath - 目标节点路径 * @param params.component - 组件类名,支持精确匹配('cc.Label')和模糊匹配('label') * @returns 添加成功后的组件信息 * * @example * ```ts * // 通过节点路径 + 精确组件名 - * const comp = await addComponent({ nodePathOrUuid: 'Canvas/MyNode', component: 'cc.Label' }); + * const comp = await add({ nodePath: 'Canvas/MyNode', component: 'cc.Label' }); * - * // 通过节点 UUID + 模糊组件名 - * const comp = await addComponent({ nodePathOrUuid: 'abc-123-uuid', component: 'label' }); + * // 通过节点路径 + 模糊组件名 + * const comp = await add({ nodePath: 'Canvas/MyNode', component: 'label' }); * ``` */ - addComponent(params: IAddComponentOptions): Promise; + add(params: IAddComponentOptions): Promise; /** * 删除指定组件 * @param params - 删除组件选项 - * @param params.path - 组件路径,支持路径、UUID 或资源 URL + * @param params.path - 组件路径 * @returns 删除成功返回 true,失败返回 false */ - removeComponent(params: IRemoveComponentOptions): Promise; + remove(params: IRemoveComponentOptions): Promise; /** - * 设置组件属性 - * - CLI 调用时传入 ISetPropertyOptions,通过 componentPath 定位,属性为扁平键值对 - * - 编辑器调用时传入 ISetPropertyOptionsForEditor,通过节点 UUID + dump 路径定位,属性为 IProperty 格式 + * 设置组件属性(编辑器格式) + * 通过节点路径 + dump 路径定位,属性为 IProperty 格式 * - * @param params - 设置属性选项,根据调用方不同传入不同类型 + * @param params - 设置属性选项 * @returns 设置成功返回 true,失败返回 false * * @example * ```ts - * // CLI 方式:通过 componentPath 定位,直接传属性键值对 * await setProperty({ - * componentPath: 'Canvas/cc.Label_1', - * properties: { string: 'Hello', fontSize: 32 }, - * }); - * - * // 编辑器方式:通过节点 UUID + dump 路径定位,传 IProperty 格式 - * await setProperty({ - * uuid: 'node-uuid', + * nodePath: 'Canvas/MyNode', * path: '__comps__.0.string', * dump: { value: 'Hello', type: 'String' }, * }); * ``` */ - setProperty(params: ISetPropertyOptions | ISetPropertyOptionsForEditor): Promise; + setProperty(params: ISetPropertyOptions): Promise; /** * 查询组件信息 - * - 传入 IQueryComponentOptions 时,返回 IComponent 或 IComponent - * - 传入 string 时,返回 IComponentForEditor + * - 传入 IQueryComponentOptions 时,返回 IComponentInfo + * - 传入 string 时,返回 IComponent * * @param params - 查询选项或组件路径字符串 - * @returns 如果传入的是 IQueryComponentOptions 时返回 IComponent,如果传入是string时返回 IComponentForEditor,未找到返回 null + * @returns 如果传入的是 IQueryComponentOptions 时返回 IComponentInfo,如果传入是string时返回 IComponent,未找到返回 null * * @example * ```ts - * CLI 模式:返回 IComponent(扁平属性) - * const comp = await queryComponent({ path: 'Canvas/cc.Label_1' }) as IComponent; + * CLI 模式:返回 IComponentInfo(扁平属性) + * const comp = await query({ path: 'Canvas/cc.Label_1' }) as IComponentInfo; * * 编辑器模式:直接传 string,这里是uuid,因为与cli重复了,也支持 path 和 url - * const comp = await queryComponent('uuid') as IComponentForEditor; + * const comp = await query('uuid') as IComponent; * ``` */ - queryComponent(params: IQueryComponentOptions | string): Promise; + query(params: IQueryComponentOptions | string): Promise; /** * 获取所有已注册的组件类名,包含内置与自定义组件 * @returns 组件类名数组,如 ['cc.Label', 'cc.Sprite', 'MyCustomComponent'] */ - queryAllComponent(): Promise; + queryAll(): Promise; // ---- 编辑器相关接口 ---- - /** - * 创建组件(编辑器使用),与 addComponent 不同的是仅返回是否成功 - * @param params - 添加组件选项 - * @param params.nodePathOrUuid - 目标节点路径或 UUID - * @param params.component - 组件类名 - * @returns 创建成功返回 true,失败返回 false - */ - createComponent(params: IAddComponentOptions): Promise; - /** * 复位组件,将组件所有属性恢复为默认值 * @param params - 查询组件选项,用于定位要复位的组件 - * @param params.path - 组件路径,支持路径、UUID 或资源 URL + * @param params.path - 组件路径 * @returns 复位成功返回 true,失败返回 false */ - resetComponent(params: IQueryComponentOptions): Promise; + reset(params: IQueryComponentOptions): Promise; /** * 获取所有注册类名,支持按继承关系过滤 @@ -243,27 +203,27 @@ export interface IComponentService extends IServiceEvents { /** * 查询指定节点上所有组件暴露的可调用函数 - * @param uuid - 节点 UUID + * @param path - 节点路径 * @returns 节点上组件的函数信息,节点不存在时返回空对象 */ - queryComponentFunctionOfNode(uuid: string): Promise; + queryFunctionOfNode(path: string): Promise; /** * 执行组件上的指定方法 * @param options - 执行选项 - * @param options.uuid - 组件实例的 UUID + * @param options.path - 组件路径,如 'Canvas/cc.Label_1' * @param options.name - 要执行的方法名,如 'onLoad'、'start' * @param options.args - 方法参数列表 * @returns 执行成功返回 true,失败返回 false */ - executeComponentMethod(options: IExecuteComponentMethodOptions): Promise; + executeMethod(options: IExecuteComponentMethodOptions): Promise; /** * 查询指定名称的组件是否已注册(是否存在对应脚本) * @param name - 组件类名,如 'cc.Label' * @returns 存在返回 true,不存在返回 false */ - queryComponentHasScript(name: string): Promise; + hasScript(name: string): Promise; // ---- 内部接口,不对外暴露 ---- diff --git a/src/core/scene/common/editor/index.ts b/src/core/scene/common/editor/index.ts index addbbd8c4..7b778053e 100644 --- a/src/core/scene/common/editor/index.ts +++ b/src/core/scene/common/editor/index.ts @@ -1,4 +1,4 @@ -import { IScene } from './scene'; +import type { IScene } from './scene'; import type { Node, Scene } from 'cc'; import type { INode } from '../node'; import type { ICloseOptions, ICreateOptions, IOpenOptions, IReloadOptions, ISaveOptions } from './options'; diff --git a/src/core/scene/common/editor/scene.ts b/src/core/scene/common/editor/scene.ts index ac4333e75..0ebb84762 100644 --- a/src/core/scene/common/editor/scene.ts +++ b/src/core/scene/common/editor/scene.ts @@ -1,14 +1,18 @@ -import type { INode } from '../node'; -import type { IComponentIdentifier } from '../component'; -import type { IBaseIdentifier } from './base'; -import { IPrefabInfo } from '../prefab'; +import { ITargetOverrideInfo } from '../prefab'; +import { IProperty } from '../../@types/public'; -/** - * 场景信息 - */ -export interface IScene extends IBaseIdentifier { - name: string; - prefab: IPrefabInfo | null, - children: INode[]; - components: IComponentIdentifier[]; +export interface IScene { + path: string; + name: IProperty; + active: IProperty; + locked: IProperty; + _globals: Record; + isScene: boolean; + autoReleaseAssets: IProperty; + + uuid: IProperty; + children: IProperty[]; + parent: string; + __type__: string; + targetOverrides?: ITargetOverrideInfo[]; } diff --git a/src/core/scene/common/index.ts b/src/core/scene/common/index.ts index 4a582095d..663b10aa4 100644 --- a/src/core/scene/common/index.ts +++ b/src/core/scene/common/index.ts @@ -1,4 +1,5 @@ export * from './const'; +export * from './cli'; export * from './node'; export * from './prefab'; export * from './editor'; diff --git a/src/core/scene/common/node.ts b/src/core/scene/common/node.ts index 4171aeffd..8aa30fa61 100644 --- a/src/core/scene/common/node.ts +++ b/src/core/scene/common/node.ts @@ -1,8 +1,10 @@ import type { Node } from 'cc'; -import { IComponent, IComponentIdentifier } from './component'; +import { IRemovedComponentInfo, ISetPropertyOptions } from './component'; import { IVec3, IQuat } from './value-types'; import { IServiceEvents } from '../scene-process/service/core'; -import { IPrefabInfo, IPrefabStateInfo } from './prefab'; +import { IPrefabStateInfo, ITargetOverrideInfo } from './prefab'; +import type { IProperty } from '../@types/public'; +import type { IScene } from './editor/scene'; // ====== Hierarchy tree types (for queryNodeTree) ====== @@ -115,13 +117,6 @@ export interface INodeProperties { // readonly activeInHierarchy: boolean; // 节点在场景中是否激活 } -// 节点标识符接口 -export interface INodeIdentifier { - nodeId: string; // 节点的 id - path: string; // 节点在场景中的路径 - name: string; // 节点名称 -} - // 节点查询参数接口 export interface IQueryNodeParams { path: string; // 查询的节点路径 @@ -129,12 +124,44 @@ export interface IQueryNodeParams { queryComponent: boolean; // 是否查询component的详细信息 } -// 节点查询结果项接口 -export interface INode extends INodeIdentifier { - properties: INodeProperties; // 节点属性 - components?: IComponent[] | IComponentIdentifier[]; // 节点上的组件列表 - children?: INode[]; // 子节点列表 - prefab: IPrefabInfo | null;// 是否是预制体 +export interface IPrefab { + uuid: string; + fileId: string; + rootUuid: string; + sync: boolean; + prefabStateInfo: IPrefabStateInfo; + targetOverrides?: ITargetOverrideInfo[]; + instance?: IProperty; +} + +export interface INode { + path: string; + active: IProperty; + locked: IProperty; + name: IProperty; + position: IProperty; + + /** + * 此为 dump 数据,非 node.rotation + * 实际指向 node.eulerAngles + * rotation 为了给用户更友好的文案 + */ + rotation: IProperty; + mobility: IProperty; + + scale: IProperty; + layer: IProperty; + uuid: IProperty; + + children: IProperty[]; + parent: IProperty; + + __comps__: IProperty[]; + __type__: string; + __prefab__?: IPrefab; + _prefabInstance?: any; + removedComponents?: IRemovedComponentInfo[]; + mountedRoot?: string; } // 节点更新参数接口 @@ -216,7 +243,16 @@ export interface INodeEvents { 'node:removed': [Node, IChangeNodeOptions]; } -export interface IPublicNodeService extends Omit {} +export interface IPublicNodeService extends Omit { +} /** * 节点的相关处理接口 @@ -226,32 +262,152 @@ export interface INodeService extends IServiceEvents { * 创建节点 * @param params */ - createNodeByType(params: ICreateByNodeTypeParams): Promise; + createByType(params: ICreateByNodeTypeParams): Promise; /** * 创建节点 * @param params */ - createNodeByAsset(params: ICreateByAssetParams): Promise; + createByAsset(params: ICreateByAssetParams): Promise; /** * 删除节点 - * @param params + * @param params */ - deleteNode(params: IDeleteNodeParams): Promise; + delete(params: IDeleteNodeParams): Promise; /** * 更新节点 * @param params */ - updateNode(params: IUpdateNodeParams): Promise; + update(params: IUpdateNodeParams): Promise; /** - * 查询节点 - */ - queryNode(params: IQueryNodeParams): Promise; + * 查询节点信息 + * + * @param params - 查询选项 + * @returns 查询到的节点信息,未找到返回 null + */ + query(params?: IQueryNodeParams): Promise; /** * 查询节点树(层级管理器格式) */ queryNodeTree(params: IQueryNodeTreeParams): Promise; + + // ---- 编辑器相关接口 ---- + + /** + * 预览设置节点属性,临时应用属性变更但不记录到 undo 栈 + * 用于编辑器中拖拽滑块等实时预览场景,首次调用时会缓存原始值, + * 可通过 cancelPreviewSetProperty 恢复 + * + * @param options - 设置属性选项 + * @param options.nodePath - 节点路径 + * @param options.path - 属性路径,如 'position'、'scale' + * @param options.dump - 属性的 dump 数据 + * @returns 设置成功返回 true,节点或属性路径无效返回 false + * + * @example + * ```ts + * // 预览修改节点位置 + * await previewSetProperty({ + * nodePath: 'Canvas/MyNode', + * path: 'position', + * dump: { value: { x: 100, y: 200, z: 0 }, type: 'cc.Vec3' }, + * }); + * ``` + */ + previewSetProperty(options: ISetPropertyOptions): Promise; + + /** + * 取消预览设置,将节点属性恢复到 previewSetProperty 调用前的值 + * 仅使用 options.nodePath 和 options.path,options.dump 不会被使用 + * + * @param options - 设置属性选项 + * @param options.nodePath - 节点路径 + * @param options.path - 属性路径 + * @returns 恢复成功返回 true,无缓存的预览数据或节点无效返回 false + */ + cancelPreviewSetProperty(options: ISetPropertyOptions): Promise; + + /** + * 设置节点属性,会记录到 undo 栈 + * + * @param options - 设置属性选项 + * @param options.nodePath - 节点路径 + * @param options.path - 属性路径,如 'position'、'rotation'、'layer' + * @param options.dump - 属性的 dump 数据 + * @returns 设置成功返回 true,节点不存在返回 false + * + * @example + * ```ts + * await setProperty({ + * nodePath: 'Canvas/MyNode', + * path: 'position', + * dump: { value: { x: 100, y: 200, z: 0 }, type: 'cc.Vec3' }, + * }); + * ``` + */ + setProperty(options: ISetPropertyOptions): Promise; + + /** + * 重置节点的变换属性(position、rotation、scale、mobility)到默认值 + * + * @param path - 节点路径 + * @returns 重置成功返回 true,节点不存在返回 false + */ + reset(path: string): Promise; + + /** + * 重置节点的单个属性到 CCClass 定义的默认值 + * 仅使用 options.nodePath 和 options.path,options.dump 不会被使用 + * + * @param options - 设置属性选项 + * @param options.nodePath - 节点路径 + * @param options.path - 属性路径,如 'position'、'scale' + * @returns 重置成功返回 true,节点不存在返回 false + */ + resetProperty(options: ISetPropertyOptions): Promise; + + /** + * 将节点上值为 null 的属性初始化为默认实例 + * 当属性为 null 且有定义构造函数类型时,会创建该类型的新实例 + * 仅使用 options.nodePath 和 options.path,options.dump 不会被使用 + * + * @param options - 设置属性选项 + * @param options.nodePath - 节点路径 + * @param options.path - 属性路径 + * @returns 初始化成功返回 true,节点不存在返回 false + * + * @example + * ```ts + * // 将节点上值为 null 的自定义属性初始化 + * await updatePropertyFromNull({ + * nodePath: 'Canvas/MyNode', + * path: 'customProperty', + * dump: {} as IProperty, + * }); + * ``` + */ + updatePropertyFromNull(options: ISetPropertyOptions): Promise; + + /** + * 设置节点及其所有子节点的 layer 属性 + * 递归将相同的 layer 值应用到整个节点子树 + * 仅使用 options.nodePath 和 options.dump,options.path 不会被使用(内部固定为 'layer') + * + * @param options - 设置属性选项 + * @param options.nodePath - 节点路径 + * @param options.dump - layer 属性的 dump 数据 + * + * @example + * ```ts + * await setNodeAndChildrenLayer({ + * nodePath: 'Canvas/MyNode', + * path: 'layer', + * dump: { value: 1 << 25, type: 'Enum' }, + * }); + * ``` + */ + setNodeAndChildrenLayer(options: ISetPropertyOptions): Promise; } /// diff --git a/src/core/scene/common/prefab/index.ts b/src/core/scene/common/prefab/index.ts index da25484bd..6adeaf4dc 100644 --- a/src/core/scene/common/prefab/index.ts +++ b/src/core/scene/common/prefab/index.ts @@ -1,3 +1,14 @@ export * from './service'; export * from './params'; export * from './prefab-info'; +export { + OptimizationPolicy, + ITargetInfo, + IMountedChildrenInfo, + IMountedComponentsInfo, + IPropertyOverrideInfo, + IPrefabInstance, + IPrefabInfo, + IPrefabDetail, + ITargetOverrideDetail, +} from '../cli/prefab'; diff --git a/src/core/scene/common/prefab/prefab-info.ts b/src/core/scene/common/prefab/prefab-info.ts index ce23c3990..b4a48120a 100644 --- a/src/core/scene/common/prefab/prefab-info.ts +++ b/src/core/scene/common/prefab/prefab-info.ts @@ -1,69 +1,13 @@ -import type { INodeIdentifier } from '../node'; -import type { IComponentIdentifier } from '../component'; - -export enum OptimizationPolicy { - AUTO = 0, - SINGLE_INSTANCE = 1, - MULTI_INSTANCE = 2, -} - -export interface IPrefabInstance { - fileId: string; - prefabRootNode?: INodeIdentifier; - mountedChildren: IMountedChildrenInfo[]; - mountedComponents: IMountedComponentsInfo[]; - propertyOverrides: IPropertyOverrideInfo[]; - removedComponents: ITargetInfo[]; -} - -export interface IMountedChildrenInfo { - targetInfo: ITargetInfo | null; - nodes: INodeIdentifier[]; -} - -export interface IPropertyOverrideInfo { - targetInfo: ITargetInfo | null; - propertyPath: string[]; - value?: any; -} - -export interface ITargetInfo { - localID: string[]; -} - export interface ICompPrefabInfo { fileId: string; } -export interface IMountedComponentsInfo { - targetInfo: ITargetInfo | null; - components: IComponentIdentifier[]; -} - export interface ITargetOverrideInfo { - source: IComponentIdentifier | INodeIdentifier | null; - sourceInfo: ITargetInfo | null; + source: string; + sourceInfo?: string[]; propertyPath: string[]; - target: INodeIdentifier | null; - targetInfo: ITargetInfo | null; -} - -export interface IPrefab { - name: string; - uuid: string; - data: INodeIdentifier, - optimizationPolicy: OptimizationPolicy, - persistent: boolean, -} - -export interface IPrefabInfo { - /** 关联的预制体资源信息 */ - asset?: IPrefab; - root?: INodeIdentifier; - instance?: IPrefabInstance; - fileId: string; - targetOverrides: ITargetOverrideInfo[]; - nestedPrefabInstanceRoots: INodeIdentifier[]; + target: string; + targetInfo?: string[]; } export enum PrefabState { diff --git a/src/core/scene/common/prefab/service.ts b/src/core/scene/common/prefab/service.ts index 521392060..f1f6e589f 100644 --- a/src/core/scene/common/prefab/service.ts +++ b/src/core/scene/common/prefab/service.ts @@ -1,7 +1,6 @@ import type { Node } from 'cc'; import type { IServiceEvents } from '../../scene-process/service/core'; -import type { IPrefabInfo } from './prefab-info'; -import { INode } from '../node'; +import { INode, IPrefab } from '../node'; import type { IApplyPrefabChangesParams, ICreatePrefabFromNodeParams, @@ -18,7 +17,8 @@ export interface IPrefabEvents { } -export interface IPublicPrefabService extends Omit { } +export interface IPublicPrefabService extends Omit { +} export interface IPrefabService extends IServiceEvents { /** @@ -49,7 +49,7 @@ export interface IPrefabService extends IServiceEvents { /** * 获取节点的预制体信息 */ - getPrefabInfo(params: IGetPrefabInfoParams): Promise; + getPrefabInfo(params: IGetPrefabInfoParams): Promise; /** * 移除 prefab info diff --git a/src/core/scene/i18n/en.js b/src/core/scene/i18n/en.js new file mode 100644 index 000000000..80efaacc1 --- /dev/null +++ b/src/core/scene/i18n/en.js @@ -0,0 +1,430 @@ +'use strict'; + +module.exports = { + + ...require('./en/node'), + + title: 'Scene', + description: 'Cocos Creator Scene Editor', + preview_title: 'Preview', + dock: 'DOCK', + + project_2d_name: '2D Project', + project_2d_tooltip: + 'Current Project is type of 2D Project.
If you want to switch the current project to a 3D project,
check "Basic 3D Features" module checkbox from the menu:
Project / Project Settings / Feature Cropping', + + new: 'New Scene', + save: 'Save Scene', + save_as: 'Save As', + align_with_view: 'Align Node with Scene View', + align_view_with_node: 'Align Scene View with Node', + + is3DValueWarn: '2D mode is active. This 3D parameter is currently in use. Please check if this aligns with your expectations.', + distribution: 'Distribute:', + alignment: 'Align:', + + dragDrop: { + typeMismatch: 'The types do not match. Please ensure the asset type is the same to perform this action.' + }, + + menu: { + undo: 'Undo', + redo: 'Redo', + + // 新建菜单 + newNodeEmpty: 'Empty Node', + + new3dObject: '3D Object', + new3dCube: 'Cube', + new3dCylinder: 'Cylinder', + new3dSphere: 'Sphere', + new3dCapsule: 'Capsule', + new3dCone: 'Cone', + new3dTorus: 'Torus', + new3dPlane: 'Plane', + new3dQuad: 'Quad', + + newLightObject: 'Light', + newLightDirectional: 'Directional Light', + newLightSphere: 'Sphere Light', + newLightSpot: 'Spot Light', + + newLightProbe: 'Light Probe Group', + newReflectionProbe: 'Reflection Probe', + + newCameraObject: 'Camera', + newTerrain: 'Terrain', + + newEffects: 'Effects', + newEffectsParticle: 'Particle System', + + newUI: 'UI Component', + newRenderUI: '2D Object', + newUICanvas: 'Canvas', + newUISprite: 'Sprite', + newUILabel: 'Label', + newUIButton: 'Button', + newUIToggle: 'Toggle', + newUIToggleGroup: 'ToggleGroup', + newUISlider: 'Slider', + newUIProgressBar: 'ProgressBar', + newUIWidget: 'Widget', + newUIEditBox: 'EditBox', + newUILayout: 'Layout', + newUIScrollView: 'ScrollView', + newUIMask: 'Mask', + newUIParticle2D: 'ParticleSystem2D', + newUISpriteSplash: 'SpriteSplash', + newUIRichText: 'RichText', + newUITiledMap: 'TiledMap', + newUIVideoPlayer: 'VideoPlayer', + newUIWebView: 'WebView', + newUIPageView: 'PageView', + newUIGraphics: 'Graphics', + + newSpriteRenderer: 'SpriteRenderer', + + experimental: 'Experimental', + + help_url: 'Help Document', + }, + + develop: 'Open Scene DevTools', + preview_develop: 'Open Preview DevTools', + graphical_tools: 'Toggle Graphics Tool', + + terrain: { + is_create_message: 'You need terrain assets, when you visit terrain editor, do you want to create it?', + is_create: 'Do you need to create terrain asset?', + path_unlegal: 'Please limit the saved path to the current project assets path', + cancel: 'Cancel', + edit: 'Edit', + save: 'Save', + delete: 'Delete', + abort: 'Abort', + manage: 'Manage', + bulge: 'Paint Bulge', + sunken: 'Paint Sunken', + smooth: 'Paint Smooth', + paint: 'Paint', + sculpt: 'Sculpt', + select: 'Select', + noImageData: 'No Data', + + tileSize: 'Tile Size', + weightMapSize: 'Weight Map Size', + lightMapSize: 'Light Map Size', + blockCount: 'Block Count', + brushSize: 'Brush Size', + brushStrength: 'Brush Strength', + brushHeight: 'Brush Height', + brushMode: 'Brush Mode', + brushRotation: 'Brush Rotation', + brushFalloff: 'Brush Falloff', + brush: 'Brush', + layer: 'Terrain Layer', + normalMap: 'Normal Map', + metallic: 'Metallic', + roughness: 'Roughness', + paintTileSize: 'Tile Size', + index: 'Index', + layers: 'Layers', + weight: 'Weight', + }, + messages: { + cannot_cut_to_self: 'Can not cut node and paste to isself', + warning: 'Warning', + scenario_modified: ' data has been modified.', + want_to_save: 'Do you want to save data to the file.?', + save: 'Save', + dont_save: 'Don\'t save', + cancel: 'Cancel', + save_fail: 'Failed to save scene: please limit the saved path to the current project assets path and suffix it with \'.scene\'', + save_fail_prefab: + 'Failed to save prefab: please limit the saved path to the current project assets path and suffix it with \'.prefab\'', + save_type_fail: 'New scene save type mismatch', + + confirm: 'Confirm', + particle_system_2d: { + export_error: + 'This resource does not support exports outside of the project,please limit the saved path to the current project assets path and suffix it with.plist', + }, + scene_cache: { + use_latest_scene: + 'It is found that the scene {url} to be opened has unsaved scene data generated by {time}. Do you want to apply this data?', + use_last_scene: + 'Failed to open the scene ({url}). The scene data may be damaged. We found the scene has data of the historical cached version ({time}), do you apply it?', + apply: 'Apply', + no: 'No', + }, + not_response: 'Scene Not Response', + debug_native: 'Open C++ debug tool,debug with attaching process id', + graphical_tools_not_support: 'Editor preview and native scene does not support graphical tools yet', + + webGLContextLost: { + message: 'WebGL context of the scene view has been lost. Do you want to reload it to restore the context?', + title: 'WebGL Context Lost', + buttons: { + reload: 'Reload', + cancel: 'Cancel' + } + }, + + setInternalCameraParent: 'It is forbidden to modify the parent node of the internal camera node', + + loadSceneTimeoutTips: { + message: 'Open scene timeout. Do you want to wait?', + waiting: 'waiting', + interrupt: 'interrupt' + } + }, + + save_prefab: 'Save', + close_prefab: 'Close', + save_clip: 'Save', + close_clip: 'Close', + + gizmos: { + tools_visibility_3d: '3D Tools Visibility', + icon3d: '3D Icons', + showGrid: 'Show Grid', + showOriginAxis: 'Show Origin', + }, + ui_tools: { + zoom_up: 'Zoom up', + zoom_down: 'Zoom down', + zoom_reset: 'Actual Size', + align_top: 'Align Top', + align_v_center: 'Align Vertical Center', + align_bottom: 'Align Bottom', + align_left: 'Align Left', + align_h_center: 'Align Horizontal Center', + align_right: 'Align Right', + distribute_top: 'Distribute Top', + distribute_v_center: 'Distribute Vertical Center', + distribute_bottom: 'Distribute Bottom', + distribute_left: 'Distribute Left', + distribute_h_center: 'Distribute Horizontal Center', + distribute_right: 'Distribute Right', + }, + tooltips: { + view_gizmo: 'Switch between View Mode and Select Objects Mode with shortcut key Q', + translate_gizmo: 'Move Gizmo, drag gizmo handle to change Node\'s position. (W)', + rotate_gizmo: 'Rotate Gizmo, drag gizmo handle to change Node\'s rotation. (E)', + scale_gizmo: 'Scale Gizmo, drag gizmo handle to change Node\'s scale. (R)', + rect_gizmo: 'Rect Gizmo, drag edges or corner points to change Node\'s size and position. (T)', + local: 'Local', + local_gizmo: 'Gizmo\'s rotation is relative to the Node\'s.', + global: 'Global', + global_gizmo: 'Gizmo\'s rotation stay the same as world space orientation.', + pivot: 'Pivot', + pivotTip: 'Position the Gizmo at the actual anchor point of a Node', + center: 'Center', + centerTip: 'Position the Gizmo at the center of the object’s rendered bounds.', + edit_mode: 'Switch 2D/3D Edit Mode. (2)', + rotationTip3D: 'The degree of rotation in local space.
The corresponding scripting API is Node.eulerAngles.', + rotationTip2D: 'The degree of rotation in counter clockwise.
The corresponding scripting API is Node.angle.', + scaleTip: 'The scaling of this node in local space', + positionTip: 'Position coordinates in local space', + }, + + increment_snap: { + title: 'Increment Snap Configuration', + enable_translate: 'Enable snap when moving', + enable_rotate: 'Enable snap when rotating', + enable_scale: 'Enable snap when scaling', + xyz_together: 'X, Y, and Z all use X value', + }, + + rect_tool_snap: { + title: 'Rect Tool Snap Configuration', + enable_snap: 'Enable auto snap', + threshold: 'Snap threshold', + }, + + scripting: { + crReport: 'Possible circular-reference in {importer}: when import {imported} from {source}.', + }, + + camera_size: { + render_target_resolution: 'Render target resolution', + }, + + scene_view: { + is_scene_light_on: 'If toggle on, all the light in the scene is used, otherwise use a directional light align with editor camera', + }, + + animation: { + delete_edit_clip_limit: 'Can not remove the animation clip being edited in the animation editing mode', + }, + + debug_view: { + base_shading: 'Basic Shading', + shaded: 'Shaded', + wireframe: 'Wireframe', + wireframe_on_shaded: 'Wireframe on Shaded', + performance_info: 'Performance Info', + overdraw: 'Overdraw', + mipMap_density: 'MipMap Density', + UV_density: 'UV Density', + lightMap_density: 'LightMap Density', + light_map_uv: 'Light Map UV', + normalMap: 'Normal Map', + + physics_info: 'Physics Info', + collision: 'Collision', + rendering_debug_options: 'Rendering Debug (surface shader only)', + rendering_single_option: 'Rendering Single Option', + CSM_layer_coloration: 'CSM Layer Coloration', + lighting_with_base_color: 'Lighting with Base Color', + disable_all_single_options: 'Disable All Single Options', + model_info: 'Model Info', + vertex_colors: 'Vertex Colors', + world_normal: 'World Normal', + world_tangent: 'World Tangent', + world_position: 'World Position', + mirrored_normal: 'Mirrored Normal', + UV0: 'UV0', + UV1: 'UV1', + projection_depth_z: 'Projection Depth Z', + liner_depth_w: 'Liner Depth W', + front_face_coloration: 'Front Face Coloration', + + material_info: 'Material Info', + world_space_pixel_normals: 'World Space Pixel Normals', + world_space_pixel_tangents: 'World Space Pixel Tangents', + world_space_pixel_binormals: 'World Space Pixel Binormals', + base_color: 'Base Color', + diffuse_color: 'Diffuse Color', + specular_color: 'Specular Color', + opacity: 'Opacity', + metallic: 'Metallic', + roughness: 'Roughness', + specular_intensity: 'Specular Intensity', + ior: 'Index of Refractivity', + + lighting_info: 'Lighting Info', + direct_diffuse: 'Direct Diffuse', + direct_specular: 'Direct Specular', + direct_lighting: 'Direct Lighting', + ambient_diffuse: 'Ambient Diffuse', + ambient_specular: 'Ambient Specular', + ambient_lighting: 'Ambient Lighting', + emissive: 'Emissive', + light_map: 'Light Map', + shadows: 'Shadows', + ambient_occlusion: 'Ambient Occlusion', + + adv_lighting_info: 'Advanced Lighting Info', + fresnel: 'Fresnel', + direct_transmit_diffuse: 'Direct Transmittance', + direct_transmit_specular: 'Direct Refract', + ambient_transmit_diffuse: 'Ambient Transmittance', + ambient_transmit_specular: 'Ambient Refract', + transmit_lighting: 'Transmittance Lighting', + direct_trt: 'Direct Internal Reflection', + ambient_trt: 'Ambient Internal Reflection', + trt_lighting: 'Internal Reflection Lighting', + tt_lighting: 'Internal Transmit Lighting', + + misc_info: 'Misc Info', + fog_factor: 'Fog', + + rendering_composite_options: 'Rendering Composite Options', + enable_all_composite_options: 'Enable All Composite Options', + lighting: 'Lighting', + tone_mapping: 'Tone Mapping', + cammacorrection: 'GammaCorrection', + transmit_diffuse: 'Transmittance Lighting', + transmit_specular: 'Refract', + }, + + game_view: { + edit: 'Edit...', + design_resolution: 'Design Resolution', + free_aspect: 'Free Aspect', + full_screen_tips: 'Fit to current view', + devtool_invalid: 'preview devtool is available only in editor preview mode', + ready: 'preview process is ready', + failed: 'preview process inited failed', + }, + + editor_camera: { + title: 'Scene Camera', + fov: 'Fov', + fovTip: '
fov
Field of view of the camera.', + far: 'Far', + farTip: '
far
Far clipping distance of the camera,
should be as small as possible within acceptable range.', + near: 'Near', + nearTip: '
near
Near clipping distance of the camera,
should be as large as possible within acceptable range.', + color: 'Color', + colorTip: '
color
Clearing color of the camera.', + wheel: 'Wheel', + wheelTip: '
wheelSpeed
The speed of the camera moving in the scene view.', + wander: 'Wander', + wanderTip: '
wanderSpeed
The speed at which the camera roams the scene view.', + enableAcceleration: 'Acceleration', + enableAccelerationTip: '
enableAcceleration
Wander Acceleration. ' + + 'If enabled, editor camera will accelerate over time when moving,
otherwise camera will move in a constant speed.', + aperture: 'Aperture', + apertureTip: '
aperture
Camera aperture, controls the exposure parameter.', + shutter: 'Shutter', + shutterTip: '
shutter
Camera shutter, controls the exposure parameter.', + iso: 'Iso', + isoTip: '
iso
Camera ISO, controls the exposure parameter.', + + settings: { + reset: 'Reset Settings' + } + }, + + contributions: { + ...require('./en/contributions/messages'), + ...require('./en/contributions/preferences'), + ...require('./en/contributions/console'), + }, + + lod: { + culled: 'Culled', + }, + crash: { + dialog: { + native_crash: { + message: 'Native engine is used to improve editor\'s performance. However, we detect multiple scene crashes. Do you want to use Typescript engine for editor instead?', + switch_to_ts: 'Switch to Typescript engine and restart', + continue: 'Continue to use native engine', + }, + }, + }, + disable_in_native_tooltip: 'This function is temporarily unavailable when native engine mode is enabled for the editor', + ui_prop: { + array_not_support_multiple: 'Arrays do not support multiple selections', + }, + + shortcut: { + camera_wander: 'Wandering:', + wander_speed: 'Speed Up:', + wander_wheelUp: 'Increase Speed:', + wander_wheelDown: 'Decrease Speed:', + vertexSnap: 'Vertex Snap:', + surfaceSnap: 'Surface Snap:', + }, + + // 属性的右键菜单 + property_contextmenu: { + copy_property_path: 'Copy Property Path', + copy_property_value: 'Copy Property Value', + paste_property_value: 'Paste Property Value', + }, + multi_scene: { + title: 'CocosCreator reload', + tips: 'Changing the multi-scene editing mode requires editor restart. please restart yourself.', + confirm: 'Confirm', + }, + multi_tab_contextmenu: { + locate_resource: 'Locate to the Resource', + close: 'Close', + close_to_right: 'Close Scenes to the Right', + close_others: 'Close Other Scenes', + }, +}; diff --git a/src/core/scene/i18n/en/contributions/console.js b/src/core/scene/i18n/en/contributions/console.js new file mode 100644 index 000000000..fce252c49 --- /dev/null +++ b/src/core/scene/i18n/en/contributions/console.js @@ -0,0 +1,5 @@ +module.exports = { + console: { + clearOnPlay: 'Clear on Play', + }, +} \ No newline at end of file diff --git a/src/core/scene/i18n/en/contributions/messages.js b/src/core/scene/i18n/en/contributions/messages.js new file mode 100644 index 000000000..d708bd1e7 --- /dev/null +++ b/src/core/scene/i18n/en/contributions/messages.js @@ -0,0 +1,570 @@ +module.exports = { + messages: { + description: { + gameview_stop: 'Stop GameView', + gameview_play_of_switch_scene: 'The GameView is currently in the playing state. To switch scenes, please stop GameView.', + open_scene: 'Open scene', + close_scene: 'Close scene', + save_scene: 'Save scene', + save_as_scene: 'Save scene to other place', + query_is_ready: 'Query the ready state of current scene', + query_dirty: 'Query dirty state of current scene', + query_classes: 'Query all classes', + query_components: 'Query all components', + query_component_has_script: 'Whether a script is in the components list', + query_node_tree: 'Query node tree information', + query_node_by_asset_uuid: 'Query node by asset uuid', + set_property: 'Set property of object', + reset_property: 'reset a property of object with default value', + // update_property_from_null: 'update a property value of object from null', + // set_node_and_children_layer: 'Set the layer of node and it\'s children', + move_array_element: 'Move the position of item in the property with Array type', + remove_array_element: 'Remove the item of property with Array type', + cut_node: 'Cut node', + // select_all_nodes: 'Select all nodes', + copy_node: 'Copy node, prepare data for paste or create node', + duplicate_node: 'Duplicate node', + paste_node: 'Paste node', + set_parent: 'Set parent of node', + create_node: 'Create node', + query_node: 'Query dump data of node', + reset_node: 'Reset node properties: position, rotation, scale.', + remove_node: 'Remove node', + create_component: 'Create component', + reset_component: 'Reset component', + execute_component_method: 'Execute method of component', + execute_scene_script: 'Execute method of extension script', + query_component: 'Query dump data of component', + snapshot: 'Snapshot current scene state', + snapshot_abort: 'Abort snapshot', + // undo: 'Undo operation', + // redo: 'Redo operation', + soft_reload: 'Soft reload scene', + change_gizmo_tool: 'Change gizmo tool', + change_gizmo_pivot: 'Change gizmo pivot', + change_gizmo_coordinate: 'Change gizmo coordinate', + change_is2D: 'Change between 2D/3D view', + set_grid_visible: 'Show or hide grid', + query_is_grid_visible: 'Query visible state of grid', + set_icon_gizmo_3d: 'Set the IconGizmo to 3D or 2D', + query_is_icon_gizmo_3d: 'Query IconGizmo mode', + set_icon_gizmo_size: 'Set the size of IconGizmo', + query_icon_gizmo_size: 'Query size of IconGizmo', + query_gizmo_tool_name: 'Query current gizmo tool name', + query_gizmo_view_mode: 'Query view mode (view/select)', + query_gizmo_pivot: 'Query current gizmo pivot name', + query_gizmo_coordinate: 'Query current gizmo coordinate name', + query_is2D: 'Query current view mode(2D/3D)', + focus_camera: 'Focus editor camera to nodes', + align_with_view: 'Apply the scene camera position and Angle to the selected node', + align_view_with_node: 'Applies the selected node position and Angle to the current view', + // broadcast + scene_ready: 'Message when scene is opened', + scene_close: 'Message when scene is closed', + + UITransform_lack: 'UI node is being added, but the cc.UITransform component is not found in any upper node', + UITransform_add_to_root: 'Add cc.UITransform component to the root node', + UITransform_within_canvas: 'Create the Canvas node as the parent node', + UITransform_cancel: 'Cancel', + + animationComponentCollision: + 'Animation controller component, animation component and skeleton animation component cannot coexist.', + + physicsDynamicBodyShape: 'A dynamic rigid body can not have the following collider shapes: Terrain, Plane and Non-Convex Mesh.', + + light_probe_edit_mode_changed: 'LightProbe edit mode changed notification', + light_probe_bounding_box_edit_mode_changed: 'LightProbe component bounding box edit mode changed notification', + light_probe_delete_when_editing_probe: 'Currently in probe editing mode The probe node being edited cannot be modified. Please exit probe editing mode and try again.', + + begin_recording: 'Begin node recording for undo', + end_recording: 'End node recording for undo', + cancel_recording: 'Cancel node recording for undo', + + // prefab + create_prefab: 'Create prefab asset(record undo automatically)', + apply_prefab: 'Apply modification to prefab asset(record undo automatically)', + restore_prefab: 'Restore prefab node form asset(record undo automatically)', + revert_removed_component: 'Revert removed component(record undo automatically)', + apply_removed_component: 'Apply removed component to prefab asset(record undo automatically)', + }, + doc: { + open_scene: ` + - uuid {string} uuid of scene asset`, + query_classes: ` + @returns {[Object]} + - extends? {string} filter classes which extend from this class + `, + query_components: ` + @returns {[Object]} + - name {string} name of component + - path {string} path in menu + `, + query_component_has_script: ` + - name class name of script + + @returns {boolean} exist or not + `, + query_node_tree: ` + - uuid? {string} the uuid of root node, default is scene node + + @returns {Object} + - name {string} name of node or 'scene' + - active {boolean} active state of node + - type {string} cc.Scene or cc.Node + - uuid {string} uuid of node + - children {[]} children of current node + - prefab {number} state of prefab, 1: normal, 2: lost resource + - isScene {boolean} whether it is a scene node + - components {[Object]} array of component + - type {string} type of component + - value {string} uuid of component + - extends {[string]} array of component inheritance chain + `, + query_node_by_asset_uuid: ` + - Query node by asset uuid + + @returns {string[]} uuid of node + `, + set_property: ` + - options {SetPropertyOptions} + - uuid {string} uuid of the object + - path {string} search path of the property + - dump {IProperty} the dump data of the property + `, + reset_property: ` + - options {SetPropertyOptions} + - uuid {string} uuid of the object + - path {string} search path of the property + `, + // update_property_from_null: ` + // - options {SetPropertyOptions} + // - uuid {string} uuid of the object + // - path {string} search path of the property + // `, + // set_node_and_children_layer: ` + // - options {SetPropertyOptions} + // - uuid {string} uuid of the object + // - path {string} search path of the property + // - dump {IProperty} the dump data of the property + // `, + move_array_element: ` + - options {MoveArrayOptions} + - uuid {string} uuid of node + - path {string} search path of array + - target {number} original index of the target item + - offset {number} move offset + + @returns {boolean} whether it is successful + `, + remove_array_element: ` + - options {MoveArrayOptions} + - uuid {string} uuid of node + - path {string} search path of array + - index {number} index of item + + @returns {boolean} whether it is successful + `, + // select_all_nodes: ` + // - Select all nodes. In different scene modes, the selected node types are different. For example, in Light Probe Editor mode, only Light Probe nodes are selected. + + // @returns {string[]} uuid of nodes + // `, + copy_node: ` + - uuids {string | string[]} uuid of node + + @returns {string | string[]} uuid of node + `, + cut_node: ` + - uuids {string | string[]} uuid of node + + @returns {string | string[]} uuid of node + `, + duplicate_node: ` + - uuids {string | string[]} uuid of node + + @returns {string | string[]} uuid of new node + `, + paste_node: ` + - options {PasteNodeOptions} + - target {string} uuid of target node + - uuids {string | string[]} uuid of node that is copied + - keepWorldTransform {boolean} whether to keep the world transform + + @returns {string | string[]} uuid of new node + `, + set_parent: ` + - options {CutNodeOptions} + - parent {string} uuid of parent + - uuids {string|string[]} uuid of the node that need to set + - keepWorldTransform {boolean} whether to keep the world transform + + @returns {string | string[]} uuid of node + `, + create_node: ` + - options {CreateNodeOptions} + - parent {string} uuid of parent + - components? {string[]} component names + + - name? {string} name of node + - dump? {INode | IScene} dump data of node + - keepWorldTransform? {boolean} whether to keep the world transform + - type? {string} asset type + - canvasRequired? {boolean} need cc.Canvas or not + - unlinkPrefab? {boolean} to be a normal node + - assetUuid? {string} uuid of asset, if this value is set, create node from this asset + + @returns {string | string[]} uuid of node + `, + query_node: ` + - uuid {string} uuid of node + + @returns {Object} dump of node + `, + reset_node: ` + - uuid {string} uuid of node + + @returns {boolean} whether it is successful + `, + restore_prefab: ` + - uuid {string} uuid of node + - assetUuid {string} uuid of asset + + @returns {boolean} whether it is successful + `, + remove_node: ` + - options {RemoveNodeOptions} + - uuid: {string | string[]} uuid of node + `, + create_component: ` + - options {CreateComponentOptions} + - uuid {string} uuid of node + - component {string} classId (cid) (is recommended) or className + `, + remove_component: ` + - options {CreateComponentOptions} + - uuid {string} uuid of node + - component {string} classId (cid) (is recommended) or className + `, + reset_component: ` + - options {ResetComponentOptions} + - uuid {string} uuid of component + + @returns {boolean} whether it is successful + `, + execute_component_method: ` + - options {ExecuteComponentMethodOptions} + - uuid {string} uuid of component + - name {string} name of method + - args {any[]} arguments + `, + execute_scene_script: ` + - options {ExecuteSceneScriptMethodsOptions} + - name {string} name of extension + - method {string} name of method + - args {any[]} arguments + `, + query_component: ` + - uuid {string} uuid of component + + @returns {Object} dump of component + `, + change_gizmo_tool: ` + - name {string} tool name 'position' | 'rotation' | 'scale' | 'rect' + `, + change_gizmo_pivot: ` + - name {string} pivot name 'pivot' | 'center' + `, + change_gizmo_coordinate: ` + - type {string} coordinate name 'local' | 'global' + `, + change_is2D: ` + - is2D {boolean} 2D/3D view + `, + set_grid_visible: ` + - visible {boolean} show/hide grid + `, + query_is_grid_visible: ` + @returns {boolean} true: visible, false: invisible + `, + set_icon_gizmo_3d: ` + - is3D {boolean} 3D/2D IconGizmo + `, + query_is_icon_gizmo_3d: ` + @returns {boolean} true: 3D, false: 2D + `, + set_icon_gizmo_size: ` + - size {number} size of IconGizmo + `, + query_icon_gizmo_size: ` + @returns {number} size of IconGizmo + `, + query_gizmo_tool_name: ` + @returns {string} 'position' | 'rotation' | 'scale' | 'rect' + `, + query_gizmo_view_mode: ` + @return {string} 'view' | 'select' + `, + query_gizmo_pivot: ` + @returns {string} 'pivot' | 'center' + `, + query_gizmo_coordinate: ` + @returns {string} 'local' | 'global' + `, + query_is2D: ` + @returns {boolean} true:2D, false:3D + `, + focus_camera: ` + - uuids {string[] | null} uuid of node + `, + align_with_view: ` + @returns {null} + `, + align_view_with_node: ` + @returns {null} + `, + + // broadcast + scene_ready: ` + - uuid {string} uuid of scene + `, + + light_probe_edit_mode_changed: ` + - mode {boolean} light probe edit mode after changed + `, + + light_probe_bounding_box_edit_mode_changed: ` + - mode {boolean} light probe component bounding box edit mode after changed + `, + }, + example: { + // message + open_scene: ` +await Editor.Message.request('scene', 'open-scene', sceneUuid); + `, + save_scene: ` +await Editor.Message.request('scene', 'save-scene'); + `, + save_as_scene: ` +await Editor.Message.request('scene', 'save-as-scene'); + `, + close_scene: ` +await Editor.Message.request('scene', 'close-scene'); + `, + query_is_ready: ` +await Editor.Message.request('scene', 'query-is-ready'); + `, + query_dirty: ` +await Editor.Message.request('scene', 'query-dirty'); + `, + query_classes: ` +await Editor.Message.request('scene', 'query-classes'); + `, + query_components: ` +await Editor.Message.request('scene', 'query-components'); + `, + query_component_has_script: ` +await Editor.Message.request('scene', 'query-component-has-script', 'cc.Sprite'); + `, + query_node_tree: ` +await Editor.Message.request('scene', 'query-node-tree', nodeUuid); + `, + query_node_by_asset_uuid: ` +await Editor.Message.request('scene', 'query-nodes-by-asset-uuid', assetUuid); + `, + set_property: ` +await Editor.Message.request('scene', 'set-property', { + uuid: nodeUuid, + path: '__comps__.1.defaultClip', + dump: { + type: 'cc.AnimationClip', + value: { + uuid: animClipUuid, + }, + }, +}); + `, + reset_property: ` +await Editor.Message.request('scene', 'reset-property', { + uuid: nodeUuid, + path: 'position', +}); + `, + move_array_element: ` +await Editor.Message.request('scene', 'move-array-element', { + uuid: nodeUuid, + path: '__comps__', + target: 1, + offset: -1, +}); + `, + remove_array_element: ` +await Editor.Message.request('scene', 'remove-array-element', { + uuid: nodeUuid, + path: '__comps__', + index: 0, +}); + `, + copy_node: ` +await Editor.Message.request('scene', 'copy-node', uuids); + `, + cut_node: ` +await Editor.Message.request('scene', 'cut-node', uuids); + `, + duplicate_node: ` +await Editor.Message.request('scene', 'duplicate-node', uuids); + `, + paste_node: ` +await Editor.Message.request('scene', 'paste-node', { + target: nodeUuid, + uuids: nodeUuids, +}); + `, + set_parent: ` +await Editor.Message.request('scene','set-parent', { + parent: nodeUuid, + uuids: nodeUuids, +}); + `, + create_node: ` +await Editor.Message.request('scene', 'create-node', { + name: 'New Node' + parent: nodeUuid, +}); + `, + query_node: ` +await Editor.Message.request('scene', 'query-node', nodeUuid); + `, + reset_node: ` +await Editor.Message.request('scene', 'reset-node', { + uuid: nodeUuid, +}); + `, + restore_prefab: ` +await Editor.Message.request('scene', 'restore-prefab', nodeUuid, assetUuid); + `, + remove_node: ` +await Editor.Message.request('scene', 'remove-node', { + uuid: nodeUuid +}); + `, + create_component: ` +Editor.Message.request('scene', 'create-component', { + uuid: nodeUuid, + component: 'cc.Sprite' +}); + `, + remove_component: ` +await Editor.Message.request('scene', 'remove-component', { + uuid: componentUuid, +}); + `, + reset_component: ` +await Editor.Message.request('scene', 'reset-component', { + uuid: componentUuid, +}); + `, + execute_component_method: ` +await Editor.Message.request('scene', 'execute-component-method', { + uuid: componentUuid, + name: 'getNoisePreview', + args: [100, 100], +}); + `, + execute_scene_script: ` +await Editor.Message.request('scene', 'execute-scene-script', { + name: 'animation-graph', + method: 'query', + args: [], +}); + `, + snapshot: ` +await Editor.Message.request('scene', 'snapshot'); + `, + snapshot_abort: ` +await Editor.Message.request('scene', 'snapshot-abort'); + `, + begin_recording: ` +const undoID = await Editor.Message.request('scene', 'begin-recording', nodeUuid); + `, + end_recording: ` +await Editor.Message.request('scene', 'end-recording', undoID); + `, + cancel_recording: ` +await Editor.Message.request('scene', 'cancel-recording', undoID); + `, + soft_reload: ` +await Editor.Message.request('scene', 'soft-reload'); + `, + query_component: ` +await Editor.Message.request('scene', 'query-component', nodeUuid); + `, + change_gizmo_tool: ` +await Editor.Message.request('scene', 'change-gizmo-tool', 'position'); + `, + change_gizmo_pivot: ` +await Editor.Message.request('scene', 'change-gizmo-pivot'); + `, + change_gizmo_coordinate: ` +await Editor.Message.request('scene', 'change-gizmo-coordinate', 'global'); + `, + change_is2D: ` +await Editor.Message.request('scene', 'change-is2D', true); + `, + set_grid_visible: ` +await Editor.Message.request('scene', 'set-grid-visible', false); + `, + query_is_grid_visible: ` +await Editor.Message.request('scene', 'query-is-grid-visible'); + `, + set_icon_gizmo_3d: ` +await Editor.Message.request('scene', 'set-icon-gizmo-3d', false); + `, + query_is_icon_gizmo_3d: ` +await Editor.Message.request('scene', 'query-is-icon-gizmo-3d'); + `, + set_icon_gizmo_size: ` +await Editor.Message.request('scene', 'set-icon-gizmo-size', 60); + `, + query_icon_gizmo_size: ` +await Editor.Message.request('scene', 'query-icon-gizmo-size'); + `, + query_gizmo_tool_name: ` +await Editor.Message.request('scene', 'query-gizmo-tool-name'); + `, + query_gizmo_view_mode: ` +await Editor.Message.request('scene', 'query-gizmo-view-mode'); + `, + query_gizmo_pivot: ` +await Editor.Message.request('scene', 'query-gizmo-pivot'); + `, + query_gizmo_coordinate: ` +await Editor.Message.request('scene', 'query-gizmo-coordinate'); + `, + query_is2D: ` +await Editor.Message.request('scene', 'query-is2D'); + `, + focus_camera: ` +await Editor.Message.request('scene', 'focus-camera', nodeUuids); + `, + align_with_view: ` +await Editor.Message.request('scene', 'align-with-view'); + `, + align_view_with_node: ` +await Editor.Message.request('scene', 'align-with-view-node'); + `, + // broadcast + scene_ready: ` +Editor.Message.broadcast('scene:ready', assetUuid); + `, + scene_close: ` +Editor.Message.broadcast('scene:close'); + `, + light_probe_edit_mode_changed: ` +Editor.Message.broadcast('scene:light-probe-edit-mode-changed', true); + `, + light_probe_bounding_box_edit_mode_changed: ` +Editor.Message.broadcast('scene:light-probe-bounding-box-edit-mode-changed', true); + `, + }, + }, +} \ No newline at end of file diff --git a/src/core/scene/i18n/en/contributions/preferences.js b/src/core/scene/i18n/en/contributions/preferences.js new file mode 100644 index 000000000..d48aad6d9 --- /dev/null +++ b/src/core/scene/i18n/en/contributions/preferences.js @@ -0,0 +1,15 @@ +module.exports = { + preferences: { + scene_cache: { + use: 'Scene real-time cache', + interval: 'Scene real-time cache interval time', + }, + scene: { + debug_native: 'Debug native engine for scene editor', + native: 'Use native engine for scene editor', + on_native_change: 'The native engine for the scene editor configuration has been modified and requires a reboot of the editor to take effect', + tick: 'Keep scene is mainloop running', + multi: 'Enabled multi scene editing', + }, + }, +} \ No newline at end of file diff --git a/src/core/scene/i18n/en/node.js b/src/core/scene/i18n/en/node.js new file mode 100644 index 000000000..07bc01c69 --- /dev/null +++ b/src/core/scene/i18n/en/node.js @@ -0,0 +1,29 @@ +module.exports = { + 'cc': { + 'Node': { + 'properties': { + 'position': { + displayName: 'Position', + tooltip: 'Position coordinates in local space.', + }, + 'eulerAngles': { + displayName: 'Rotation', + tooltip2D: 'The degree of rotation in counter clockwise.', + tooltip3D: 'The degree of rotation in local space.', + }, + 'scale': { + displayName: 'Scale', + tooltip: 'The scaling of this node in local space.', + }, + 'mobility': { + displayName: 'Mobility', + tooltip: 'Node\'s mobility', + }, + 'layer': { + displayName: 'Layer', + tooltip: 'Layer of the current Node, it affects raycast, physics etc.', + }, + } + } + } +} \ No newline at end of file diff --git a/src/core/scene/i18n/zh.js b/src/core/scene/i18n/zh.js new file mode 100644 index 000000000..471618444 --- /dev/null +++ b/src/core/scene/i18n/zh.js @@ -0,0 +1,424 @@ +'use strict'; + +module.exports = { + + ...require('./zh/node'), + + title: '场景编辑器', + description: 'Cocos Creator 场景编辑器', + preview_title: '预览', + dock: '停靠', + + project_2d_name: '2D 项目', + project_2d_tooltip: + '当前项目为 2D 项目
引擎的 3D 模块将会在构建时剔除,编辑器已屏蔽部分 3D 相关功能。
如需切换为 3D 项目,可在菜单:
项目 / 项目设置 / 功能裁剪 中勾选 “基础 3D 功能” 模块。', + + new: '新建场景', + save: '保存场景', + save_as: '另存为..', + align_with_view: '将节点对齐到场景视角', + align_view_with_node: '将场景视角对齐到与节点', + + is3DValueWarn: '正在使用 2D 模式,该 3D 参数正被使用,请检查是否符合你的预期', + + distribution: '分布:', + alignment: '对齐:', + + dragDrop: { + typeMismatch: '类型不同,请确保资源类型相同才能执行此操作。' + }, + + menu: { + undo: '撤销', + redo: '重做', + + // 新建菜单 + newNodeEmpty: '空节点', + + new3dObject: '3D 对象', + new3dCube: 'Cube 立方体', + new3dCylinder: 'Cylinder 圆柱体', + new3dSphere: 'Sphere 球体', + new3dCapsule: 'Capsule 胶囊', + new3dCone: 'Cone 圆锥体', + new3dTorus: 'Torus 圆环体', + new3dPlane: 'Plane 平面', + new3dQuad: 'Quad 四方形', + + newLightObject: '光源', + newLightDirectional: '平行光', + newLightSphere: '球面光', + newLightSpot: '聚光', + + newLightProbe: '光照探针', + newReflectionProbe: '反射探针', + + newCameraObject: '摄像机', + newTerrain: '地形', + + newEffects: '特效', + newEffectsParticle: '粒子系统', + + newUI: 'UI 组件', + newRenderUI: '2D 对象', + newUICanvas: 'Canvas(画布)', + newUISprite: 'Sprite(精灵)', + newUILabel: 'Label(文本)', + newUIButton: 'Button(按钮)', + newUIToggle: 'Toggle(复选按钮)', + newUIToggleGroup: 'ToggleGroup(单选按钮)', + newUISlider: 'Slider(滑动器)', + newUIProgressBar: 'ProgressBar(进度条)', + newUIWidget: 'Widget(对齐)', + newUIEditBox: 'EditBox(输入框)', + newUILayout: 'Layout(布局)', + newUIScrollView: 'ScrollView(滚动视图)', + newUIMask: 'Mask(遮罩)', + newUIParticle2D: 'ParticleSystem2D(粒子)', + newUISpriteSplash: 'SpriteSplash(单色)', + newUIRichText: 'RichText(富文本)', + newUITiledMap: 'TiledMap(地图)', + newUIVideoPlayer: 'VideoPlayer(播放器)', + newUIWebView: 'WebView(网页视图)', + newUIPageView: 'PageView(页面视图)', + newUIGraphics: 'Graphics(绘图)', + + newSpriteRenderer: 'SpriteRenderer(2D精灵)', + + experimental: '实验性功能', + + help_url: '帮助文档', + }, + + develop: '打开场景调试工具', + preview_develop: '打开预览调试工具', + graphical_tools: '开关图形工具', + + terrain: { + is_create_message: '编辑地形需要有地形资源,是否创建地形资源?', + is_create: '是否创建地形资源?', + path_unlegal: '保存路径请限制在当前项目 /assets 路径内', + cancel: '取消', + edit: '编辑', + save: '保存', + delete: '删除', + abort: '中止', + manage: '管理', + bulge: '雕塑 隆起', + sunken: '雕塑 凹下', + smooth: '雕塑 平滑', + paint: '涂料', + sculpt: '雕塑', + select: '选择', + noImageData: '暂无数据', + + tileSize: '栅格大小', + weightMapSize: '权重图大小', + lightMapSize: '光照图大小', + blockCount: '地形块数量', + brushSize: '画刷大小', + brushStrength: '画刷强度', + brushHeight: '画刷高度', + brushMode: '画刷模式', + brushRotation: '画刷旋转', + brushFalloff: '画刷衰减', + brush: '画刷', + layer: '纹理层', + normalMap: '法线贴图', + metallic: '金属性', + roughness: '粗糙度', + paintTileSize: '平铺大小', + index: '索引', + layers: '纹理层', + weight: '权重图', + }, + messages: { + cannot_cut_to_self: '不能将剪切的节点粘贴到自己身上', + warning: '警告', + scenario_modified: ' 数据已经修改。', + want_to_save: '是否要把数据保存到文件?', + save: '保存', + dont_save: '不保存', + cancel: '取消', + save_fail: '保存场景失败:保存路径请限制在当前项目 /assets 路径内,并以 .scene 作为文件后缀', + save_fail_prefab: '保存预制件失败:保存路径请限制在当前项目 /assets 路径内,并以 .prefab 作为文件后缀', + save_type_fail: '新场景保存类型不匹配', + + confirm: '确定', + particle_system_2d: { + export_error: '该资源不支持导出到项目外,保存路径请限制在当前项目 /assets 路径内,并以 .plist 作为文件后缀', + }, + scene_cache: { + use_latest_scene: '查询到即将打开的场景 {url} 存在 {time} 生成的未被保存的场景数据,是否应用该数据?', + use_last_scene: '打开 {url} 场景失败,该场景数据可能已经损坏,查询到有历史缓存版本 ({time})的场景数据,是否应用?', + apply: '应用', + no: '否', + }, + not_response: '场景无响应', + debug_native: '调试原生场景方法:请打开C++调试工具,附加到场景进程后,点击确定', + graphical_tools_not_support: '编辑器预览和原生场景暂不支持抓帧', + + webGLContextLost: { + message: '场景视图的 WebGL 上下文已丢失,是否重载场景视图进行恢复?', + title: 'WebGL 上下文丢失', + buttons: { + reload: '重新加载', + cancel: '取消' + } + }, + + setInternalCameraParent: '禁止修改内置摄像机节点的父节点', + + loadSceneTimeoutTips: { + message: '打开场景超时。是否继续等待?', + waiting: '等待', + interrupt: '中断' + } + }, + + save_prefab: '保存', + close_prefab: '关闭', + save_clip: '保存', + close_clip: '关闭', + + gizmos: { + tools_visibility_3d: '3D工具可见性', + icon3d: '3D 图标', + showGrid: '显示网格', + showOriginAxis: '显示原点', + }, + + ui_tools: { + zoom_up: '放大', + zoom_down: '缩小', + zoom_reset: '按1:1显示', + align_top: '顶对齐', + align_v_center: '垂直居中对齐', + align_bottom: '底对齐', + align_left: '左对齐', + align_h_center: '水平居中对齐', + align_right: '右对齐', + distribute_top: '按顶分布', + distribute_v_center: '按垂直居中分布', + distribute_bottom: '按底分布', + distribute_left: '按左分布', + distribute_h_center: '按水平居中分布', + distribute_right: '按右分布', + }, + + tooltips: { + view_gizmo: '查看/选择对象工具,使用快捷键 Q 切换模式', + translate_gizmo: '移动工具,拖拽工具手柄来修改节点的位置 (W)', + rotate_gizmo: '旋转工具,拖拽工具手柄来修改节点的角度 (E)', + scale_gizmo: '缩放工具,拖拽工具手柄来修改节点的缩放 (R)', + rect_gizmo: '矩形变换工具,拖拽四条边或四个顶点,可以同时修改节点的大小和位置 (T)', + local: '局部坐标', + local_gizmo: '变换工具中手柄箭头的朝向表示相对于节点的方向', + global: '世界坐标', + global_gizmo: '变换工具中手柄箭头的朝向以世界坐标系为准,不会考虑节点的旋转', + pivot: '锚点', + pivotTip: '变换工具出现在节点的锚点位置', + center: '中心点', + centerTip: '变换工具出现在节点的中心点位置', + edit_mode: '切换 2D/3D 编辑模式. (2)', + }, + + increment_snap: { + title: '增量吸附配置', + enable_translate: '是否启用移动吸附', + enable_rotate: '是否启用旋转吸附', + enable_scale: '是否启用缩放吸附', + xyz_together: 'X Y Z 统一使用 X 值', + }, + + rect_tool_snap: { + title: '矩形工具吸附配置', + enable_snap: '启用智能对齐', + threshold: '吸附检测阈值', + }, + + scripting: { + crReport: '在 {importer} 中检测到可能的循环引用:从 {source} 导入 {imported} 时。', + }, + + camera_size: { + render_target_resolution: '渲染输出目标分辨率', + }, + + scene_view: { + is_scene_light_on: '如果开启,将开启场景中的灯,如果关闭则使用一个和场景相机对齐的平行光', + }, + + editor_camera: { + title: '场景摄像机', + fov: '视角大小', + fovTip: '
fov
相机的视角大小。', + far: '远焦距', + farTip: '
far
相机的远裁剪距离,应在可接受范围内尽量取最小。', + near: '近焦距', + nearTip: '
near
相机的近裁剪距离,应在可接受范围内尽量取最大。', + color: '颜色', + colorTip: '
color
相机的颜色缓冲默认值。', + wheel: '滚轮', + wheelTip: '
wheelSpeed
相机在场景视图移动的速度', + wander: '漫游', + wanderTip: '
wanderSpeed
相机漫游场景视图的速度。', + enableAcceleration: '漫游加速', + enableAccelerationTip: '
enableAcceleration
开启后,漫游相机移动速度会随着时间增长, 否则相机会以一个恒定速度移动。', + aperture: '光圈', + apertureTip: '
aperture
相机光圈,影响相机的曝光参数。', + shutter: '快门', + shutterTip: '
shutter
相机快门,影响相机的曝光参数。', + iso: '感光度', + isoTip: '
iso
相机感光度,影响相机的曝光参数。', + + settings: { + reset: '重置' + } + }, + + animation: { + delete_edit_clip_limit: '动画编辑模式下不允许移除或替换正在编辑的动画剪辑', + }, + + debug_view: { + base_shading: '基础绘制模式', + shaded: '正常渲染', + wireframe: '线框', + wireframe_on_shaded: '正常渲染加线框', + performance_info: '性能信息', + overdraw: '几何密度', + mipMap_density: '贴图密度', + UV_density: 'UV密度', + lightMap_density: 'UV密度', + normalMap: '法线贴图', + light_map_uv: '光照贴图UV', + + physics_info: '物理信息', + collision: '碰撞显示', + rendering_debug_options: '渲染调试 (只支持 surface shader)', + rendering_single_option: '渲染单项调试', + CSM_layer_coloration: '级联阴影染色', + lighting_with_base_color: '光照信息带固有色(纯光照切换)', + disable_all_single_options: '无单项调试', + model_info: '模型信息', + vertex_colors: '顶点色', + world_normal: '世界空间顶点法线', + world_tangent: '世界空间顶点切线', + world_position: '世界空间顶点坐标 ', + mirrored_normal: '法线镜像', + UV0: 'UV0', + UV1: ' UV1', + projection_depth_z: '投影深度Z', + liner_depth_w: '线性深度W', + front_face_coloration: '正反面标记', + + material_info: '材质信息', + world_space_pixel_normals: '世界空间像素法线', + world_space_pixel_tangents: '世界空间像素切线', + world_space_pixel_binormals: '世界空间像素副法线', + base_color: '固有色', + diffuse_color: '漫反射颜色', + specular_color: '镜面反射颜色', + opacity: '透明度', + metallic: '金属度', + roughness: '粗糙度', + specular_intensity: '镜面反射强度', + ior: '折射率', + + lighting_info: '光照信息', + direct_diffuse: '直接光漫反射', + direct_specular: '直接光镜面反射', + direct_lighting: '直接光照', + ambient_diffuse: '环境光漫反射', + ambient_specular: '环境光镜面反射', + ambient_lighting: '环境光照', + emissive: '自发光', + light_map: '光照贴图', + shadows: '阴影', + ambient_occlusion: '环境光遮蔽', + + adv_lighting_info: '高级光照信息', + fresnel: '菲涅耳', + direct_transmit_diffuse: '直接透射光', + direct_transmit_specular: '直接折射光', + ambient_transmit_diffuse: '环境透射光', + ambient_transmit_specular: '环境折射光', + transmit_lighting: '透射光照', + direct_trt: '直接光内反射', + ambient_trt: '环境光内反射', + trt_lighting: '内反射光照', + tt_lighting: '内透射光照', + + misc_info: '其他信息', + fog_factor: '雾', + + rendering_composite_options: '渲染组合调试', + enable_all_composite_options: '全选渲染组合调试组', + lighting: '光照功能', + tone_mapping: '色调映射', + cammacorrection: '伽玛校正', + transmit_diffuse: '透射光', + transmit_specular: '折射光', + }, + + game_view: { + edit: '管理...', + design_resolution: '设计分辨率', + free_aspect: '不限比例', + full_screen_tips: '最佳显示比例', + devtool_invalid: '预览调试工具仅在编辑器预览下可用', + ready: '预览环境初始化完毕', + failed: '预览环境初始化失败,无法预览', + }, + + contributions: { + ...require('./zh/contributions/messages'), + ...require('./zh/contributions/preferences'), + ...require('./zh/contributions/console'), + }, + + lod: { + culled: '剔除', + }, + crash: { + dialog: { + native_crash: { + message: '原生场景被用来提高场景的效果和表现,但是检测到多次场景崩溃, 是否尝试切换到typescript引擎?', + switch_to_ts: '切换并重启', + continue: '继续使用原生引擎', + }, + }, + }, + disable_in_native_tooltip: '启用原生引擎模式下该功能暂不可用', + + ui_prop: { + array_not_support_multiple: '数组暂不支持多选编辑', + }, + + shortcut: { + camera_wander: '相机漫游:', + wander_speed: '加速:', + wander_wheelUp: '提高速度:', + wander_wheelDown: '减低速度:', + vertexSnap: '顶点吸附:', + surfaceSnap: '表面吸附:', + }, + // 属性右键菜单 + property_contextmenu: { + copy_property_path: '复制属性路径', + copy_property_value: '复制值', + paste_property_value: '粘贴值', + }, + multi_scene: { + title: '编辑器重启', + tips: '修改多场景编辑模式需要重启编辑器,请自行重启', + confirm: '确认', + }, + multi_tab_contextmenu: { + locate_resource: '定位到资源', + close: '关闭', + close_to_right: '关闭右侧场景', + close_others: '关闭其他场景', + } +}; diff --git a/src/core/scene/i18n/zh/contributions/console.js b/src/core/scene/i18n/zh/contributions/console.js new file mode 100644 index 000000000..56d30fba4 --- /dev/null +++ b/src/core/scene/i18n/zh/contributions/console.js @@ -0,0 +1,5 @@ +module.exports = { + console: { + clearOnPlay: '预览时清空', + }, +} \ No newline at end of file diff --git a/src/core/scene/i18n/zh/contributions/messages.js b/src/core/scene/i18n/zh/contributions/messages.js new file mode 100644 index 000000000..ac1cc5871 --- /dev/null +++ b/src/core/scene/i18n/zh/contributions/messages.js @@ -0,0 +1,570 @@ +module.exports = { + messages: { + description: { + gameview_stop: '退出播放', + gameview_play_of_switch_scene: '当前 Game View 处于播放状态,要切换场景请退出播放。', + open_scene: '打开场景', + close_scene: '关闭场景', + save_scene: '保存场景', + save_as_scene: '场景另存为', + query_is_ready: '查询当前场景是否准备就绪', + query_dirty: '查询当前场景是否有修改', + query_classes: '查询所有在引擎中注册的类', + query_components: '查询当前场景的所有组件', + query_component_has_script: '查询引擎组件列表是否含有指定类名的脚本', + query_node_tree: '查询节点树的信息', + query_node_by_asset_uuid: '查询使用了资源 UUID 的节点', + set_property: '设置某个元素内的属性', + reset_property: '重置元素属性到默认值', + // update_property_from_null: '属性值从 null 变为一个可编辑的值', + // set_node_and_children_layer: '置某个节点连同它的子集的 Layer 属性值', + move_array_element: '移动数组内某个元素的位置', + remove_array_element: '删除数组内某个元素的位置', + cut_node: '剪切节点', + // select_all_nodes: '选择所有节点', + copy_node: '拷贝节点,给下一步粘贴(创建)节点准备数据', + duplicate_node: '复制节点', + paste_node: '粘贴节点', + set_parent: '设置节点父级', + create_node: '创建节点', + query_node: '查询一个节点的数据', + reset_node: '重置节点的位置, 角度和缩放', + remove_node: '删除节点', + create_component: '创建组件', + reset_component: '重置组件', + execute_component_method: '执行组件上的方法', + execute_scene_script: '执行某个插件注册的方法', + remove_component: '删除组件', + query_component: '查询一个组件的数据', + snapshot: '快照当前场景状态', + snapshot_abort: '中止快照', + // undo: '撤销一次操作记录', + // redo: '重做一次操作记录', + soft_reload: '软刷新场景', + change_gizmo_tool: '更改 Gizmo 工具', + change_gizmo_pivot: '更改变换基准点', + change_gizmo_coordinate: '更改坐标系', + change_is2D: '更改2D/3D视图模式', + set_grid_visible: '显示/隐藏网格', + query_is_grid_visible: '查询网格显示状态', + set_icon_gizmo_3d: '设置 IconGizmo 为 3D 或 2D 模式', + query_is_icon_gizmo_3d: '查询 IconGizmo 模式', + set_icon_gizmo_size: '设置 IconGizmo 的大小', + query_icon_gizmo_size: '查询 IconGizmo 的大小', + query_gizmo_tool_name: '获取当前 Gizmo 工具的名字', + query_gizmo_view_mode: '查询视图模式(查看/选择)', + query_gizmo_pivot: '获取当前 Gizmo 基准点名字', + query_gizmo_coordinate: '获取当前坐标系名字', + query_is2D: '获取当前视图模式', + focus_camera: '聚焦场景相机到节点上', + align_with_view: '将场景相机位置与角度应用到选中节点上', + align_view_with_node: '将选中节点位置与角度应用到当前视角', + // broadcast + scene_ready: '场景打开通知', + scene_close: '场景关闭通知', + + UITransform_lack: '正在添加 UI 节点,但所有上层节点都没有 cc.UITransform 组件', + UITransform_add_to_root: '给根节点添加 cc.UITransform 组件', + UITransform_within_canvas: '创建 Canvas 节点作为父节点', + UITransform_cancel: '取消', + + animationComponentCollision: '动画控制器组件和动画组件、骨骼动画组件不能共存。', + + physicsDynamicBodyShape: '动力学刚体不能设置为以下碰撞体:Terrain, Plane, Non-Convex Mesh。', + + light_probe_edit_mode_changed: '光照探针编辑模式切换通知', + light_probe_bounding_box_edit_mode_changed: '光照探针组件包围盒编辑模式切换通知', + light_probe_delete_when_editing_probe: '当前正在探针编辑模式 无法修改正在编辑的探针节点。请先退出探针编辑模式并再次尝试。', + + begin_recording: '开始记录节点 Undo 数据', + end_recording: '结束记录节点 Undo 数据', + cancel_recording: '取消记录节点 Undo 数据', + + // prefab + create_prefab: '创建预制体资源(内置撤销记录)', + apply_prefab: '应用预制体节点修改到对应资源(内置撤销记录)', + restore_prefab: '使用预制体资源还原对应预制件节点(内置撤销记录)', + revert_removed_component: '还原预制体节点被移除的组件(内置撤销记录)', + apply_removed_component: '应用预制体删除组件的修改到对应资源(内置撤销记录)', + }, + doc: { + // message + open_scene: ` + - uuid {string} 场景资源的 UUID`, + query_classes: ` + @returns {[Object]} + - extends? {string} 过滤出基于此类名扩展而来的类 + `, + query_components: ` + @returns {[Object]} + - name {string} 组件名字 + - path {string} 菜单路径 + `, + query_component_has_script: ` + - name 脚本的类名 Class + + @returns {boolean} 存在 true, 不存在 false + `, + query_node_tree: ` + - uuid? {string} 根节点 uuid,不传入则以场景节点为根节点 + + @returns {Object} + - name {string} 节点名字或者 'scene' + - active {boolean} 节点激活状态 + - type {string} cc.Scene or cc.Node + - uuid {string} 节点的 uuid + - children {[]} 子节点数组 + - prefab {number} prefab状态, 1 表示是 prefab, 2 表示是 prefab 但丢失资源 + - isScene {boolean} 是否是场景节点 + - components {[Object]} 组件数组 + - type {string} 组件类型 + - value {string} 组件的 uuid + - extends {[string]} 组件的继承链数组 + `, + query_node_by_asset_uuid: ` + - 查询使用了资源 UUID 的节点 + + @returns {string[]} 节点的 uuid + `, + set_property: ` + - options {SetPropertyOptions} + - uuid {string} 修改属性的对象的 uuid + - path {string} 属性挂载对象的搜索路径 + - dump {IProperty} 属性 dump 出来的数据 + `, + reset_property: ` + - options {SetPropertyOptions} + - uuid {string} 修改属性的对象的 uuid + - path {string} 属性挂载对象的搜索路径 + `, + // update_property_from_null: ` + // - options {SetPropertyOptions} + // - uuid {string} 修改属性的对象的 uuid + // - path {string} 属性挂载对象的搜索路径 + // `, + // set_node_and_children_layer: ` + // - options {SetPropertyOptions} + // - uuid {string} 修改属性的对象的 uuid + // - path {string} 属性挂载对象的搜索路径 + // - dump {IProperty} 属性 dump 出来的数据 + // `, + move_array_element: ` + - options {MoveArrayOptions} + - uuid {string} 节点的 uuid + - path {string} 数组的搜索路径 + - target {number} 目标 item 原来的索引 + - offset {number} 偏移量 + + @returns {boolean} 操作是否成功 + `, + remove_array_element: ` + - options {MoveArrayOptions} + - uuid {string} 节点的 uuid + - path {string} 数组的搜索路径 + - index {number} 目标 item 的索引 + + @returns {boolean} 操作是否成功 + `, + // select_all_nodes: ` + // - 选择所有节点,在不同的场景模式下,选到的节点类型有所区别,比如Light Probe Editor模式下,只会选到Light Probe节点 + + // @returns {string[]} 节点的 uuid + // `, + copy_node: ` + - uuids {string | string[]} 节点的 uuid + + @returns {string | string[]} 返回节点的 uuid + `, + cut_node: ` + - uuids {string | string[]} 节点的 uuid + + @returns {string | string[]} 返回节点的 uuid + `, + duplicate_node: ` + - uuids {string | string[]} 节点的 uuid + + @returns {string | string[]} 返回新节点的 uuid + `, + paste_node: ` + - options {PasteNodeOptions} + - target {string} 目标节点 uuid + - uuids {string | string[]} 被复制的节点 uuid + - keepWorldTransform {boolean} 是否保持新节点的世界坐标不变 + + @returns {string | string[]} 返回新节点的 uuid + `, + set_parent: ` + - options {CutNodeOptions} + - parent {string} 父节点 uuid + - uuids {string|string[]} 需要设置的子节点 uuid + - keepWorldTransform {boolean} 是否保持新节点的世界坐标不变 + + @returns {string | string[]} 返回节点的 uuid + `, + create_node: ` + - options {CreateNodeOptions} + - parent {string} 父节点 uuid + - components? {string[]} 组件名字 + + - name? {string} 节点名字 + - dump? {INode | IScene} node 初始化应用的 dump 数据 + - keepWorldTransform? {boolean} 是否保持新节点的世界坐标不变 + - type? {string} 资源类型 + - canvasRequired? {boolean} 是否需要有 cc.Canvas + - unlinkPrefab? {boolean} 是否要解绑为普通节点 + - assetUuid? {string} asset uuid,从资源实例化节点 + + @returns {string | string[]} 返回新节点的 uuid + `, + query_node: ` + - uuid {string} 节点的 uuid + + @returns {Object} 节点的 dump 数据 + `, + reset_node: ` + - uuid {string} 节点的 uuid + + @returns {boolean} 操作是否成功 + `, + restore_prefab: ` + - uuid {string} 节点的 uuid + - assetUuid {string} 资源的 uuid + + @returns {boolean} 操作是否成功 + `, + remove_node: ` + - options {RemoveNodeOptions} + - uuid: {string | string[]} 节点的 uuid + `, + create_component: ` + - options {CreateComponentOptions} + - uuid {string} 节点的 uuid + - component {string} 组件 classId (cid)(推荐方式) 或者 className 类名 + `, + remove_component: ` + - options {CreateComponentOptions} + - uuid {string} 节点的 uuid + - component {string} 组件 classId (cid)(推荐方式) 或者 className 类名 + `, + reset_component: ` + - options {ResetComponentOptions} + - uuid {string} 组件的 uuid + + @returns {boolean} 操作是否成功 + `, + execute_component_method: ` + - options {ExecuteComponentMethodOptions} + - uuid {string} 组件的 uuid + - name {string} 方法名 + - args {any[]} 参数 + `, + execute_scene_script: ` + - options {ExecuteSceneScriptMethodsOptions} + - name {string} 注册进来的插件名字 + - method {string} 执行的方法名字 + - args {any[]} 参数数组 + `, + query_component: ` + - uuid {string} 组件的 uuid + + @returns {Object} 组件的 dump 数据 + `, + change_gizmo_tool: ` + - name {string} 工具名字 'position' | 'rotation' | 'scale'| 'rect' + `, + change_gizmo_pivot: ` + - name {string} 变换基准点 'pivot' | 'center' + `, + change_gizmo_coordinate: ` + - type {string} 坐标系 'local' | 'global' + `, + change_is2D: ` + - is2D {boolean} 2D/3D视图 + `, + set_grid_visible: ` + - visible {boolean} 显示/隐藏网格 + `, + query_is_grid_visible: ` + @returns {boolean} true: visible, false: invisible + `, + set_icon_gizmo_3d: ` + - is3D {boolean} 3D/2D IconGizmo + `, + query_is_icon_gizmo_3d: ` + @returns {boolean} true: 3D, false: 2D + `, + set_icon_gizmo_size: ` + - size {number} IconGizmo 的大小 + `, + query_icon_gizmo_size: ` + @returns {number} IconGizmo 的大小 + `, + query_gizmo_tool_name: ` + @returns {string} 'position' | 'rotation' | 'scale' | 'rect' + `, + query_gizmo_view_mode: ` + @return {string} 'view' | 'select' + `, + query_gizmo_pivot: ` + @returns {string} 'pivot' | 'center' + `, + query_gizmo_coordinate: ` + @returns {string} 'local' | 'global' + `, + query_is2D: ` + @returns {boolean} true:2D, false:3D + `, + focus_camera: ` + - uuids {string[] | null} 节点 uuid + `, + align_with_view: ` + @returns {null} + `, + align_view_with_node: ` + @returns {null} + `, + // broadcast + scene_ready: ` + - uuid {string} uuid of scene + `, + + light_probe_edit_mode_changed: ` + - mode {boolean} 切换后的探针编辑模式 + `, + + light_probe_bounding_box_edit_mode_changed: ` + - mode {boolean} 切换后的探针组件包围盒编辑模式 + `, + }, + example: { + // message + open_scene: ` +await Editor.Message.request('scene', 'open-scene', sceneUuid); + `, + save_scene: ` +await Editor.Message.request('scene', 'save-scene'); + `, + save_as_scene: ` +await Editor.Message.request('scene', 'save-as-scene'); + `, + close_scene: ` +await Editor.Message.request('scene', 'close-scene'); + `, + query_is_ready: ` +await Editor.Message.request('scene', 'query-is-ready'); + `, + query_dirty: ` +await Editor.Message.request('scene', 'query-dirty'); + `, + query_classes: ` +await Editor.Message.request('scene', 'query-classes'); + `, + query_components: ` +await Editor.Message.request('scene', 'query-components'); + `, + query_component_has_script: ` +await Editor.Message.request('scene', 'query-component-has-script', 'cc.Sprite'); + `, + query_node_tree: ` +await Editor.Message.request('scene', 'query-node-tree', nodeUuid); + `, + query_node_by_asset_uuid: ` +await Editor.Message.request('scene', 'query-nodes-by-asset-uuid', assetUuid); + `, + set_property: ` +await Editor.Message.request('scene', 'set-property', { + uuid: nodeUuid, + path: '__comps__.1.defaultClip', + dump: { + type: 'cc.AnimationClip', + value: { + uuid: animClipUuid, + }, + }, +}); + `, + reset_property: ` +await Editor.Message.request('scene', 'reset-property', { + uuid: nodeUuid, + path: 'position', +}); + `, + move_array_element: ` +await Editor.Message.request('scene', 'move-array-element', { + uuid: nodeUuid, + path: '__comps__', + target: 1, + offset: -1, +}); + `, + remove_array_element: ` +await Editor.Message.request('scene', 'remove-array-element', { + uuid: nodeUuid, + path: '__comps__', + index: 0, +}); + `, + copy_node: ` +await Editor.Message.request('scene', 'copy-node', uuids); + `, + cut_node: ` +await Editor.Message.request('scene', 'cut-node', uuids); + `, + duplicate_node: ` +await Editor.Message.request('scene', 'duplicate-node', uuids); + `, + paste_node: ` +await Editor.Message.request('scene', 'paste-node', { + target: nodeUuid, + uuids: nodeUuids, +}); + `, + set_parent: ` +await Editor.Message.request('scene','set-parent', { + parent: nodeUuid, + uuids: nodeUuids, +}); + `, + create_node: ` +await Editor.Message.request('scene', 'create-node', { + name: 'New Node' + parent: nodeUuid, +}); + `, + query_node: ` +await Editor.Message.request('scene', 'query-node', nodeUuid); + `, + reset_node: ` +await Editor.Message.request('scene', 'reset-node', { + uuid: nodeUuid, +}); + `, + restore_prefab: ` +await Editor.Message.request('scene', 'restore-prefab', nodeUuid, assetUuid); + `, + remove_node: ` +await Editor.Message.request('scene', 'remove-node', { + uuid: nodeUuid +}); + `, + create_component: ` +Editor.Message.request('scene', 'create-component', { + uuid: nodeUuid, + component: 'cc.Sprite' +}); + `, + remove_component: ` +await Editor.Message.request('scene', 'remove-component', { + uuid: componentUuid, +}); + `, + reset_component: ` +await Editor.Message.request('scene', 'reset-component', { + uuid: componentUuid, +}); + `, + execute_component_method: ` +await Editor.Message.request('scene', 'execute-component-method', { + uuid: componentUuid, + name: 'getNoisePreview', + args: [100, 100], +}); + `, + execute_scene_script: ` +await Editor.Message.request('scene', 'execute-scene-script', { + name: 'animation-graph', + method: 'query', + args: [], +}); + `, + snapshot: ` +await Editor.Message.request('scene', 'snapshot'); + `, + snapshot_abort: ` +await Editor.Message.request('scene', 'snapshot-abort'); + `, + begin_recording: ` +const undoID = await Editor.Message.request('scene', 'begin-recording', nodeUuid); + `, + end_recording: ` +await Editor.Message.request('scene', 'end-recording', undoID); + `, + cancel_recording: ` +await Editor.Message.request('scene', 'cancel-recording', undoID); + `, + soft_reload: ` +await Editor.Message.request('scene', 'soft-reload'); + `, + query_component: ` +await Editor.Message.request('scene', 'query-component', nodeUuid); + `, + change_gizmo_tool: ` +await Editor.Message.request('scene', 'change-gizmo-tool', 'position'); + `, + change_gizmo_pivot: ` +await Editor.Message.request('scene', 'query-gizmo-pivot'); + `, + change_gizmo_coordinate: ` +await Editor.Message.request('scene', 'change-gizmo-coordinate', 'global'); + `, + change_is2D: ` +await Editor.Message.request('scene', 'change-is2D', true); + `, + set_grid_visible: ` +await Editor.Message.request('scene', 'set-grid-visible', false); + `, + query_is_grid_visible: ` +await Editor.Message.request('scene', 'query-is-grid-visible'); + `, + set_icon_gizmo_3d: ` +await Editor.Message.request('scene', 'set-icon-gizmo-3d', false); + `, + query_is_icon_gizmo_3d: ` +await Editor.Message.request('scene', 'query-is-icon-gizmo-3d'); + `, + set_icon_gizmo_size: ` +await Editor.Message.request('scene', 'set-icon-gizmo-size', 60); + `, + query_icon_gizmo_size: ` +await Editor.Message.request('scene', 'query-icon-gizmo-size'); + `, + query_gizmo_tool_name: ` +await Editor.Message.request('scene', 'query-gizmo-tool-name'); + `, + query_gizmo_view_mode: ` +await Editor.Message.request('scene', 'query-gizmo-view-mode'); + `, + query_gizmo_pivot: ` +await Editor.Message.request('scene', 'query-gizmo-pivot'); + `, + query_gizmo_coordinate: ` +await Editor.Message.request('scene', 'query-gizmo-coordinate'); + `, + query_is2D: ` +await Editor.Message.request('scene', 'query-is2D'); + `, + focus_camera: ` +await Editor.Message.request('scene', 'focus-camera', nodeUuids); + `, + align_with_view: ` +await Editor.Message.request('scene', 'align-with-view'); + `, + align_view_with_node: ` +await Editor.Message.request('scene', 'align-with-view-node'); + `, + // broadcast + scene_ready: ` +Editor.Message.broadcast('scene:ready', assetUuid); + `, + scene_close: ` +Editor.Message.broadcast('scene:close'); + `, + light_probe_edit_mode_changed: ` +Editor.Message.broadcast('scene:light-probe-edit-mode-changed', true); + `, + light_probe_bounding_box_edit_mode_changed: ` +Editor.Message.broadcast('scene:light-probe-bounding-box-edit-mode-changed', true); + `, + }, + }, +} \ No newline at end of file diff --git a/src/core/scene/i18n/zh/contributions/preferences.js b/src/core/scene/i18n/zh/contributions/preferences.js new file mode 100644 index 000000000..cbff8fb0b --- /dev/null +++ b/src/core/scene/i18n/zh/contributions/preferences.js @@ -0,0 +1,15 @@ +module.exports = { + preferences: { + scene_cache: { + use: '开启场景即时缓存功能', + interval: '场景即时缓存间隔时间', + }, + scene: { + debug_native: '调试原生引擎场景', + native: '启用原生引擎加载场景编辑器', + on_native_change: '编辑器原生场景配置已修改,需要重启编辑器才能生效', + tick: '保持场景主循环运行', + multi: '启用多场景编辑', + }, + }, +} \ No newline at end of file diff --git a/src/core/scene/i18n/zh/node.js b/src/core/scene/i18n/zh/node.js new file mode 100644 index 000000000..5d0894fbc --- /dev/null +++ b/src/core/scene/i18n/zh/node.js @@ -0,0 +1,29 @@ +module.exports = { + 'cc': { + 'Node': { + 'properties': { + 'position': { + displayName: 'Position', + tooltip: '相对父节点的位置坐标', + }, + 'eulerAngles': { + displayName: 'Rotation', + tooltip2D: '旋转角度,输入正值时逆时针旋转。', + tooltip3D: '相对父节点的旋转角度。', + }, + 'scale': { + displayName: 'Scale', + tooltip: '相对父节点的整体缩放比例', + }, + 'mobility': { + displayName: 'Mobility', + tooltip: '节点的移动性', + }, + 'layer': { + displayName: 'Layer', + tooltip: '节点所属层,主要影响射线检测、物理碰撞等', + }, + } + } + } +} \ No newline at end of file diff --git a/src/core/scene/index.ts b/src/core/scene/index.ts index 30510764d..cbf147d21 100644 --- a/src/core/scene/index.ts +++ b/src/core/scene/index.ts @@ -1,3 +1,4 @@ +import i18n from '../base/i18n'; import { sceneConfigInstance } from './scene-configs'; // 接口类型 export * from './common'; @@ -9,8 +10,25 @@ import { middlewareService } from '../../server/middleware'; import SceneMiddleware from './scene.middleware'; import SceneScriptingMiddleware from './scene.scripting.middleware'; +const i18nModules: Record Promise> = { + zh: () => import('./i18n/zh'), + en: () => import('./i18n/en'), +}; + +export async function loadSceneI18n() { + for (const [lang, loader] of Object.entries(i18nModules)) { + try { + const data = await loader(); + i18n.registerLanguagePatch(lang, 'scene', data.default || data); + } catch (error) { + console.warn(`[Scene] Failed to load scene i18n for ${lang}:`, error); + } + } +} + // 场景配置初始化 export async function init() { + await loadSceneI18n(); middlewareService.register('SceneScripting', SceneScriptingMiddleware); middlewareService.register('Scene', SceneMiddleware); await sceneConfigInstance.init(); diff --git a/src/core/scene/main-process/index.ts b/src/core/scene/main-process/index.ts index 2312ff23c..c6d14b7dd 100644 --- a/src/core/scene/main-process/index.ts +++ b/src/core/scene/main-process/index.ts @@ -10,22 +10,25 @@ import { PrefabProxy } from './proxy/prefab-proxy'; import { assetManager } from '../../assets'; import scriptManager from '../../scripting'; import { sceneConfigInstance } from '../scene-configs'; +import i18n from '../../base/i18n'; export interface IMainModule { 'assetManager': typeof assetManager; 'programming': typeof scriptManager; 'sceneConfigInstance': typeof sceneConfigInstance; + 'i18n': typeof i18n; } export const Scene = { ...EditorProxy, ...ScriptProxy, - ...NodeProxy, - ...ComponentProxy, ...AssetProxy, ...EngineProxy, ...PrefabProxy, - + // 节点相关的接口 + Node: NodeProxy, + // 组件相关的接口 + Component: ComponentProxy, // 场景进程 worker: sceneWorker, }; diff --git a/src/core/scene/main-process/proxy/component-proxy.ts b/src/core/scene/main-process/proxy/component-proxy.ts index 7eafbc498..bc2f7abcf 100644 --- a/src/core/scene/main-process/proxy/component-proxy.ts +++ b/src/core/scene/main-process/proxy/component-proxy.ts @@ -1,60 +1,76 @@ import { - IComponent, - IComponentForEditor, IAddComponentOptions, IRemoveComponentOptions, IQueryComponentOptions, - ISetPropertyOptions, IPublicComponentService, - IExecuteComponentMethodOptions, - IQueryClassesOptions, } from '../../common'; -import { IProperty } from '../../@types/public'; +import { IComponentInfo } from '../../common/cli/component'; +import { ISetPropertyOptionsInfo } from '../../common/cli/component'; import { Rpc } from '../rpc'; +import { DumpConverter } from './dump-converter'; -export const ComponentProxy: IPublicComponentService = { - addComponent(params: IAddComponentOptions): Promise { - return Rpc.getInstance().request('Component', 'addComponent', [params]); - }, +export interface IComponentProxy extends Omit { + add(params: IAddComponentOptions): Promise; + query(params: IQueryComponentOptions): Promise; + setProperty(params: ISetPropertyOptionsInfo): Promise; +} - createComponent(params: IAddComponentOptions): Promise { - return Rpc.getInstance().request('Component', 'createComponent', [params]); +export const ComponentProxy: IComponentProxy = { + async add(params: IAddComponentOptions): Promise { + const result: any = await Rpc.getInstance().request('Component', 'add', [params]); + return DumpConverter.toComponent(result); }, - removeComponent(params: IRemoveComponentOptions): Promise { - return Rpc.getInstance().request('Component', 'removeComponent', [params]); + remove(params: IRemoveComponentOptions): Promise { + return Rpc.getInstance().request('Component', 'remove', [params]); }, - queryComponent(params: IQueryComponentOptions): Promise { - return Rpc.getInstance().request('Component', 'queryComponent', [params]); + async query(params: IQueryComponentOptions): Promise { + const result: any = await Rpc.getInstance().request('Component', 'query', [params]); + if (!result) return null; + if (typeof params !== 'string') { + return DumpConverter.toComponent(result); + } + return result; }, - setProperty(params: ISetPropertyOptions): Promise { - return Rpc.getInstance().request('Component', 'setProperty', [params]); - }, + async setProperty(params: ISetPropertyOptionsInfo): Promise { + const segments = params.componentPath.split('/'); + segments.pop(); + const nodePath = segments.join('/'); - queryAllComponent(): Promise { - return Rpc.getInstance().request('Component', 'queryAllComponent'); - }, + const compDump: any = await Rpc.getInstance().request('Component', 'query', [params.componentPath]); + if (!compDump) { + throw new Error(`Component not found: ${params.componentPath}`); + } - queryClasses(options?: IQueryClassesOptions): Promise<{ name: string }[]> { - return Rpc.getInstance().request('Component', 'queryClasses', [options]); - }, + const nodeTree: any = await Rpc.getInstance().request('Node', 'queryNodeTree', [{ path: nodePath }]); + if (!nodeTree) { + throw new Error(`Node not found: ${nodePath}`); + } + const compUuid = compDump.value?.uuid?.value; + const compIndex = nodeTree.components.findIndex((c: any) => c.value === compUuid); + if (compIndex < 0) { + throw new Error(`Component index not found: ${params.componentPath}`); + } - queryComponentFunctionOfNode(uuid: string): Promise { - return Rpc.getInstance().request('Component', 'queryComponentFunctionOfNode', [uuid]); + for (const [key, value] of Object.entries(params.properties)) { + const propDef = compDump.value?.[key]; + if (!propDef) { + throw new Error(`Property '${key}' not found on component`); + } + await Rpc.getInstance().request('Component', 'setProperty', [{ + nodePath, + path: `__comps__.${compIndex}.${key}`, + dump: { ...propDef, value }, + record: params.record, + }] as any); + } + return true; }, - queryComponentHasScript(name: string): Promise { - return Rpc.getInstance().request('Component', 'queryComponentHasScript', [name]); + queryAll(): Promise { + return Rpc.getInstance().request('Component', 'queryAll'); }, - - resetComponent(params: IQueryComponentOptions): Promise { - return Rpc.getInstance().request('Component', 'resetComponent', [params]); - }, - - executeComponentMethod(params: IExecuteComponentMethodOptions): Promise { - return Rpc.getInstance().request('Component', 'executeComponentMethod', [params]); - } }; diff --git a/src/core/scene/main-process/proxy/dump-converter.ts b/src/core/scene/main-process/proxy/dump-converter.ts new file mode 100644 index 000000000..85aeaa833 --- /dev/null +++ b/src/core/scene/main-process/proxy/dump-converter.ts @@ -0,0 +1,214 @@ +'use strict'; + +import type { + INodeInfo, + INode, + IComponentInfo, + IComponent, + IComponentIdentifier, + IPrefab, + IPrefabInfo, + ITargetOverrideDetail, + ISceneInfo, +} from '../../common'; +import type { IScene } from '../../common/editor/scene'; +import type { IPropertyValueType } from '../../@types/public'; + +export interface IDumpConvertOptions { + path?: string; + children?: boolean; + fullComponents?: boolean; +} + +export class DumpConverter { + static toNode(dump: INode | IScene, options?: IDumpConvertOptions): INodeInfo { + if ('isScene' in dump && dump.isScene) { + return DumpConverter.sceneToNode(dump as IScene, options); + } + return DumpConverter.nodeToNode(dump as INode, options); + } + + static toScene(dump: IScene, options?: IDumpConvertOptions): ISceneInfo { + const d = dump as any; + const identifier = d.__identifier__ ?? {}; + const children = options?.children ?? true; + return { + assetType: identifier.assetType ?? '', + assetName: identifier.assetName ?? '', + assetUuid: identifier.assetUuid ?? '', + assetUrl: identifier.assetUrl ?? '', + name: dump.name.value as string, + prefab: DumpConverter.convertPrefab(d.__prefab__), + children: children + ? (d.__childNodes__?.map((c: INode) => DumpConverter.toNode(c, options)) ?? []) + : [], + components: d.__comps__?.map((c: any) => DumpConverter.toComponentIdentifier(c)) ?? [], + }; + } + + private static sceneToNode(dump: IScene, options?: IDumpConvertOptions): INodeInfo { + const d = dump as any; + const children = options?.children ?? true; + return { + nodeId: dump.uuid.value as string, + path: options?.path || d.__path__ || '/', + name: dump.name.value as string, + properties: { + active: dump.active.value as boolean, + position: d.__position__ || { x: 0, y: 0, z: 0 }, + rotation: d.__rotation__ || { x: 0, y: 0, z: 0, w: 1 }, + eulerAngles: { x: 0, y: 0, z: 0 }, + scale: d.__scale__ || { x: 1, y: 1, z: 1 }, + mobility: d.__mobility__ ?? 0, + layer: d.__layer__ ?? 0, + }, + children: children + ? d.__childNodes__?.map((c: INode) => DumpConverter.toNode(c, options)) + : undefined, + prefab: DumpConverter.convertPrefab(d.__prefab__), + }; + } + + private static nodeToNode(dump: INode, options?: IDumpConvertOptions): INodeInfo { + const d = dump as any; + const children = options?.children ?? true; + const fullComponents = options?.fullComponents ?? false; + return { + nodeId: dump.uuid.value as string, + path: options?.path || d.__path__ || '', + name: dump.name.value as string, + properties: { + active: dump.active.value as boolean, + position: dump.position.value, + rotation: d.__rotation__ || DumpConverter.eulerToQuat(dump.rotation.value), + eulerAngles: dump.rotation.value, + scale: dump.scale.value, + mobility: dump.mobility.value as number, + layer: dump.layer.value as number, + }, + components: fullComponents + ? (dump.__comps__?.map(c => DumpConverter.toComponent(c)) ?? []) + : (dump.__comps__?.map(c => DumpConverter.toComponentIdentifier(c)) ?? []), + children: children + ? d.__childNodes__?.map((c: any) => DumpConverter.toNode(c, options)) + : undefined, + prefab: DumpConverter.convertPrefab(dump.__prefab__), + }; + } + + static toComponent(dump: IComponent): IComponentInfo { + const d = dump as any; + const properties: { [key: string]: IPropertyValueType } = {}; + + if (dump.value && typeof dump.value === 'object') { + for (const key in dump.value) { + if (key === 'uuid' || key === 'name' || key === 'enabled') { + continue; + } + properties[key] = dump.value[key]; + } + } + + return { + cid: d.cid || '', + path: d.__component_path__ || '', + uuid: (dump.value?.uuid as any)?.value || '', + name: (dump.value?.name as any)?.value || '', + type: dump.type || '', + enabled: (dump.value?.enabled as any)?.value ?? true, + properties, + prefab: d.__compPrefab__ ?? null, + }; + } + + static toComponentIdentifier(dump: IComponent): IComponentIdentifier { + const d = dump as any; + return { + cid: d.cid || '', + path: d.__component_path__ || '', + uuid: (dump.value?.uuid as any)?.value || '', + name: (dump.value?.name as any)?.value || '', + type: dump.type || '', + enabled: (dump.value?.enabled as any)?.value ?? true, + }; + } + + static convertPrefab(prefab?: IPrefab): IPrefabInfo | null { + if (!prefab) return null; + const d = prefab as any; + return { + asset: d.__asset__ ?? undefined, + root: d.__root__?.nodeId ? d.__root__ : undefined, + instance: DumpConverter.convertPrefabInstance(prefab.instance, d.__instance__), + fileId: prefab.fileId, + targetOverrides: DumpConverter.convertTargetOverrides(prefab.targetOverrides), + nestedPrefabInstanceRoots: d.__nested_roots__ ?? [], + }; + } + + private static convertTargetOverrides(overrides?: IPrefab['targetOverrides']): ITargetOverrideDetail[] { + if (!overrides) return []; + return overrides.map(info => { + const d = info as any; + return { + source: d.__source__ ?? null, + sourceInfo: info.sourceInfo ? { localID: info.sourceInfo } : null, + propertyPath: info.propertyPath, + target: d.__target__ ?? null, + targetInfo: info.targetInfo ? { localID: info.targetInfo } : null, + }; + }); + } + + private static convertPrefabInstance(instanceDump: any, enriched: any): any { + if (!instanceDump?.value) return undefined; + const v = instanceDump.value; + return { + fileId: v.fileId?.value ?? '', + prefabRootNode: enriched?.prefabRootNode ?? undefined, + mountedChildren: (v.mountedChildren?.value ?? []).map((mc: any, i: number) => ({ + targetInfo: DumpConverter.extractTargetInfo(mc.value?.targetInfo), + nodes: enriched?.mountedChildren?.[i]?.nodes ?? [], + })), + mountedComponents: (v.mountedComponents?.value ?? []).map((mc: any, i: number) => ({ + targetInfo: DumpConverter.extractTargetInfo(mc.value?.targetInfo), + components: enriched?.mountedComponents?.[i]?.components ?? [], + })), + propertyOverrides: (v.propertyOverrides?.value ?? []).map((po: any) => ({ + targetInfo: DumpConverter.extractTargetInfo(po.value?.targetInfo), + propertyPath: po.value?.propertyPath ?? [], + })), + removedComponents: (v.removedComponents?.value ?? []).map((rc: any) => ({ + localID: DumpConverter.extractLocalID(rc), + })), + }; + } + + private static extractTargetInfo(prop: any): any { + if (!prop?.value) return null; + return { localID: DumpConverter.extractLocalID(prop) }; + } + + private static extractLocalID(prop: any): string[] { + const localID = prop?.value?.localID; + if (!localID?.value || !Array.isArray(localID.value)) return []; + return localID.value.map((item: any) => String(item.value ?? '')); + } + + private static eulerToQuat(euler: any): { x: number; y: number; z: number; w: number } { + if (!euler || typeof euler !== 'object') return { x: 0, y: 0, z: 0, w: 1 }; + const DEG2RAD = Math.PI / 180; + const halfX = (euler.x || 0) * DEG2RAD * 0.5; + const halfY = (euler.y || 0) * DEG2RAD * 0.5; + const halfZ = (euler.z || 0) * DEG2RAD * 0.5; + const cx = Math.cos(halfX), sx = Math.sin(halfX); + const cy = Math.cos(halfY), sy = Math.sin(halfY); + const cz = Math.cos(halfZ), sz = Math.sin(halfZ); + return { + x: sx * cy * cz + cx * sy * sz, + y: cx * sy * cz - sx * cy * sz, + z: cx * cy * sz - sx * sy * cz, + w: cx * cy * cz + sx * sy * sz, + }; + } +} diff --git a/src/core/scene/main-process/proxy/editor-proxy.ts b/src/core/scene/main-process/proxy/editor-proxy.ts index 55b67b643..e839582ad 100644 --- a/src/core/scene/main-process/proxy/editor-proxy.ts +++ b/src/core/scene/main-process/proxy/editor-proxy.ts @@ -5,12 +5,29 @@ import { IPublicEditorService, IReloadOptions, ISaveOptions, + ISceneInfo, + INodeInfo, } from '../../common'; import { Rpc } from '../rpc'; +import { DumpConverter, IDumpConvertOptions } from './dump-converter'; -export const EditorProxy: IPublicEditorService = { - open(params: IOpenOptions) { - return Rpc.getInstance().request('Editor', 'open', [params]); +export interface IEditorProxy extends Omit { + open(params: IOpenOptions): Promise; + queryCurrent(): Promise; +} + +function convertEditorResult(dump: any, options?: IDumpConvertOptions): ISceneInfo | INodeInfo { + if ('isScene' in dump && dump.isScene) { + return DumpConverter.toScene(dump, options); + } + return DumpConverter.toNode(dump, options); +} + +export const EditorProxy: IEditorProxy = { + async open(params: IOpenOptions) { + const result: any = await Rpc.getInstance().request('Editor', 'open', [params]); + const children = !(params.simpleNode ?? true); + return convertEditorResult(result, { children }); }, close(params: ICloseOptions) { return Rpc.getInstance().request('Editor', 'close', [params]); @@ -24,10 +41,12 @@ export const EditorProxy: IPublicEditorService = { create(params: ICreateOptions) { return Rpc.getInstance().request('Editor', 'create', [params]); }, - queryCurrent() { - return Rpc.getInstance().request('Editor', 'queryCurrent'); + async queryCurrent() { + const result: any = await Rpc.getInstance().request('Editor', 'queryCurrent'); + if (!result) return null; + return convertEditorResult(result, { children: true }); }, hasOpen() { return Rpc.getInstance().request('Editor', 'hasOpen'); } -}; \ No newline at end of file +}; diff --git a/src/core/scene/main-process/proxy/node-proxy.ts b/src/core/scene/main-process/proxy/node-proxy.ts index 0bc9779ec..f694b7d61 100644 --- a/src/core/scene/main-process/proxy/node-proxy.ts +++ b/src/core/scene/main-process/proxy/node-proxy.ts @@ -1,5 +1,4 @@ import { - INode, INodeTreeItem, ICreateByNodeTypeParams, ICreateByAssetParams, @@ -11,25 +10,37 @@ import { IDeleteNodeResult, IPublicNodeService, } from '../../common'; +import { INodeInfo } from '../../common/cli/node'; import { Rpc } from '../rpc'; +import { DumpConverter } from './dump-converter'; -export const NodeProxy: IPublicNodeService = { - createNodeByType(params: ICreateByNodeTypeParams): Promise { - return Rpc.getInstance().request('Node', 'createNodeByType', [params]); +export interface INodeProxy extends Omit { + createByType(params: ICreateByNodeTypeParams): Promise; + createByAsset(params: ICreateByAssetParams): Promise; + query(params?: IQueryNodeParams): Promise; +} + +export const NodeProxy: INodeProxy = { + async createByType(params: ICreateByNodeTypeParams): Promise { + const result: any = await Rpc.getInstance().request('Node', 'createByType', [params]); + return result ? DumpConverter.toNode(result, { children: true }) : null; }, - createNodeByAsset(params: ICreateByAssetParams): Promise { - return Rpc.getInstance().request('Node', 'createNodeByAsset', [params]); + async createByAsset(params: ICreateByAssetParams): Promise { + const result: any = await Rpc.getInstance().request('Node', 'createByAsset', [params]); + return result ? DumpConverter.toNode(result, { children: true }) : null; }, - deleteNode(params: IDeleteNodeParams): Promise { - return Rpc.getInstance().request('Node', 'deleteNode', [params]); + delete(params: IDeleteNodeParams): Promise { + return Rpc.getInstance().request('Node', 'delete', [params]); }, - updateNode(params: IUpdateNodeParams): Promise { - return Rpc.getInstance().request('Node', 'updateNode', [params]); + update(params: IUpdateNodeParams): Promise { + return Rpc.getInstance().request('Node', 'update', [params]); }, - queryNode(params: IQueryNodeParams): Promise { - return Rpc.getInstance().request('Node', 'queryNode', [params]); + async query(params?: IQueryNodeParams): Promise { + const result: any = await Rpc.getInstance().request('Node', 'query', [params]); + if (!result) return null; + return DumpConverter.toNode(result, { path: params?.path, fullComponents: true }); }, queryNodeTree(params: IQueryNodeTreeParams): Promise { return Rpc.getInstance().request('Node', 'queryNodeTree', [params]); - } + }, }; diff --git a/src/core/scene/main-process/proxy/prefab-proxy.ts b/src/core/scene/main-process/proxy/prefab-proxy.ts index 9433388e7..e9d23be74 100644 --- a/src/core/scene/main-process/proxy/prefab-proxy.ts +++ b/src/core/scene/main-process/proxy/prefab-proxy.ts @@ -1,21 +1,32 @@ import type { IApplyPrefabChangesParams, ICreatePrefabFromNodeParams, - IGetPrefabInfoParams, IIsPrefabInstanceParams, INode, + IGetPrefabInfoParams, IIsPrefabInstanceParams, IPublicPrefabService, IRevertToPrefabParams, IUnpackPrefabInstanceParams, IPrefabInfo, } from '../../common'; +import { INodeInfo } from '../../common/cli/node'; import { Rpc } from '../rpc'; +import { DumpConverter } from './dump-converter'; -export const PrefabProxy: IPublicPrefabService = { +export interface IPrefabProxy extends Omit { + createPrefabFromNode(params: ICreatePrefabFromNodeParams): Promise; + unpackPrefabInstance(params: IUnpackPrefabInstanceParams): Promise; + getPrefabInfo(params: IGetPrefabInfoParams): Promise; +} + +export const PrefabProxy: IPrefabProxy = { applyPrefabChanges(params: IApplyPrefabChangesParams): Promise { return Rpc.getInstance().request('Prefab', 'applyPrefabChanges', [params]); }, - createPrefabFromNode(params: ICreatePrefabFromNodeParams): Promise { - return Rpc.getInstance().request('Prefab', 'createPrefabFromNode', [params]); + async createPrefabFromNode(params: ICreatePrefabFromNodeParams): Promise { + const result: any = await Rpc.getInstance().request('Prefab', 'createPrefabFromNode', [params]); + return DumpConverter.toNode(result, { children: false }); }, - getPrefabInfo(params: IGetPrefabInfoParams): Promise { - return Rpc.getInstance().request('Prefab', 'getPrefabInfo', [params]); + async getPrefabInfo(params: IGetPrefabInfoParams): Promise { + const result: any = await Rpc.getInstance().request('Prefab', 'getPrefabInfo', [params]); + if (!result) return null; + return DumpConverter.convertPrefab(result); }, isPrefabInstance(params: IIsPrefabInstanceParams): Promise { return Rpc.getInstance().request('Prefab', 'isPrefabInstance', [params]); @@ -23,7 +34,8 @@ export const PrefabProxy: IPublicPrefabService = { revertToPrefab(params: IRevertToPrefabParams): Promise { return Rpc.getInstance().request('Prefab', 'revertToPrefab', [params]); }, - unpackPrefabInstance(params: IUnpackPrefabInstanceParams): Promise { - return Rpc.getInstance().request('Prefab', 'unpackPrefabInstance', [params]); + async unpackPrefabInstance(params: IUnpackPrefabInstanceParams): Promise { + const result: any = await Rpc.getInstance().request('Prefab', 'unpackPrefabInstance', [params]); + return DumpConverter.toNode(result); } }; \ No newline at end of file diff --git a/src/core/scene/main-process/rpc.ts b/src/core/scene/main-process/rpc.ts index 1f5d56b81..a76bd0c0f 100644 --- a/src/core/scene/main-process/rpc.ts +++ b/src/core/scene/main-process/rpc.ts @@ -3,6 +3,7 @@ import { ChildProcess } from 'child_process'; import { assetManager } from '../../assets'; import scriptManager from '../../scripting'; import { sceneConfigInstance } from '../scene-configs'; +import i18n from '../../base/i18n'; import type { IPublicServiceManager } from '../scene-process'; @@ -33,6 +34,7 @@ export class RpcProxy { assetManager: assetManager, programming: scriptManager, sceneConfigInstance: sceneConfigInstance, + i18n: i18n, }); console.log(`[Node] Scene Process RPC ready ${prc ? '(Attached)' : '(Detached - Web Mode)'}`); } diff --git a/src/core/scene/scene-process/service/camera.ts b/src/core/scene/scene-process/service/camera.ts index 0481322d0..8592231b7 100644 --- a/src/core/scene/scene-process/service/camera.ts +++ b/src/core/scene/scene-process/service/camera.ts @@ -1,4 +1,4 @@ -import { Canvas, Color, Layers, Node, Vec3 } from 'cc'; +import { Canvas, Color, Layers, Vec3 } from 'cc'; import { BaseService } from './core'; import { register, Service } from './core/decorator'; import { CameraController2D } from './camera/camera-controller-2d'; diff --git a/src/core/scene/scene-process/service/component.ts b/src/core/scene/scene-process/service/component.ts index 0a3d947c3..aa6945913 100644 --- a/src/core/scene/scene-process/service/component.ts +++ b/src/core/scene/scene-process/service/component.ts @@ -4,17 +4,16 @@ import { register, Service, BaseService } from './core'; import { IComponentEvents, IAddComponentOptions, - IComponent, IComponentService, IQueryComponentOptions, IRemoveComponentOptions, - ISetPropertyOptions, NodeEventType, + NodeEventType, IExecuteComponentMethodOptions, - IComponentForEditor, + IComponent, IQueryClassesOptions, - ISetPropertyOptionsForEditor + ISetPropertyOptions } from '../../common'; -import dumpUtil from './dump'; +import dumpUtil, { translateDumpI18n } from './dump'; import compMgr from './component/index'; import componentUtils from './component/utils'; import getComponentFunctionOfNode from './component/get-component-function-of-node'; @@ -22,7 +21,6 @@ import { hasOneKindOfComponent } from './node/node-utils'; import { isEditorNode } from './node/node-utils'; import { createShouldHideInHierarchyCanvasNode } from './node/node-create'; import PrefabService from './prefab'; -import { IProperty } from '../../@types/public'; const NodeMgr = EditorExtends.Node; enum SceneModeType { @@ -57,15 +55,7 @@ export class ComponentService extends BaseService implements I public modeName: SceneModeType = SceneModeType.General; // private _stagingCameraInfo: any; protected _sceneEventListener: ISceneEvents[] = []; - protected _recycleComponent: Record = {}; - - constructor() { - super(); - compMgr.on('add', this.onAddComponent.bind(this)); - compMgr.on('remove', this.onRemoveComponent.bind(this)); - compMgr.on('added', this.onComponentAdded.bind(this)); - compMgr.on('removed', this.onComponentRemoved.bind(this)); - } + /** * 查询当前正在编辑的模式名字 @@ -93,9 +83,7 @@ export class ComponentService extends BaseService implements I opts.modeName = this.modeName; // TODO(qgh): 发送消息 //this.dispatchEvents('onComponentAdded', comp, opts); - if (this._recycleComponent[comp.uuid]) { - delete this._recycleComponent[comp.uuid]; - } + compMgr.addRecycleComponent(comp.uuid); } public onComponentRemoved(comp: Component, opts: IOptionBase = {}) { @@ -104,7 +92,7 @@ export class ComponentService extends BaseService implements I // this.dispatchEvents('onComponentRemoved', comp); // 编辑器中的this._sceneProxy.getRootNode()实现返回的是null PrefabService.onComponentRemovedInGeneralMode(comp, null); - this._recycleComponent[comp.uuid] = comp; + compMgr.removeRecycleComponent(comp.uuid, comp); } public dispatchEvents(eventName: keyof ISceneEvents, ...args: any[any]) { @@ -116,22 +104,17 @@ export class ComponentService extends BaseService implements I }); } - private async addComponentImpl(nodePathOrUuid: string, component: string): Promise { - const node = NodeMgr.getNodeByPath(nodePathOrUuid) ?? NodeMgr.getNode(nodePathOrUuid); - if (!node) { - throw new Error(`add component failed: ${nodePathOrUuid} does not exist`); - } - if (!component || component.length <= 0) { - throw new Error(`add component failed: ${component} does not exist`); - } - // 需要单独处理 missing script + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + private requireComponentList: Function[] = []; + + private async resolveComponentCtor(component: string): Promise> { if (component === 'MissingScript' || component === 'cc.MissingScript') { - throw new Error('Reset Component failed: MissingScript does not exist'); + throw new Error('MissingScript does not exist'); } - // 处理 URL 与 Uuid const isURL = component.startsWith('db://'); const isUuid = componentUtils.isUUID(component); + let resolvedName = component; let uuid; if (isUuid) { uuid = component; @@ -140,174 +123,122 @@ export class ComponentService extends BaseService implements I } let ctor = null; - let comp = null; if (uuid) { const cid = await Service.Script.queryScriptCid(uuid); if (cid && cid !== 'MissingScript' && cid !== 'cc.MissingScript') { - component = cid; - ctor = cc.js.getClassById(cid); + resolvedName = cid; + ctor = cc.js.getClassById(cid) || cc.js.getClassByName(cid); if (!ctor) { - ctor = cc.js.getClassByName(cid); - } - if (!ctor) { - // 理论上不会出现这个错误,出现了需要定位下 - throw `Component script(${cid}) name exists but constructor does not exist.`; + throw new Error(`Component script(${cid}) name exists but constructor does not exist.`); } } else { - // uuid存在,脚本也存在,但是组件ID不存在,则表示异常 const assetInfo = await Rpc.getInstance().request('assetManager', 'queryAssetInfo', [uuid]); if (assetInfo?.file && assetInfo?.file.length > 0) { - throw `Check if the script(${uuid}) contains any errors.`; + throw new Error(`Check if the script(${uuid}) contains any errors.`); } } } else { - ctor = cc.js.getClassById(component); - if (!ctor) { - ctor = cc.js.getClassByName(component); - } + ctor = cc.js.getClassById(resolvedName) || cc.js.getClassByName(resolvedName); } if (!ctor) { - // 首字母是否大写 - const isStartWithUppercase = (component.charAt(0) == component.charAt(0).toUpperCase()); + const isStartWithUppercase = resolvedName.charAt(0) === resolvedName.charAt(0).toUpperCase(); if (!isStartWithUppercase) { - // 首字母大写查询 - const fullName = component.charAt(0).toUpperCase() + component.slice(1); - ctor = cc.js.getClassByName(fullName); + ctor = cc.js.getClassByName(resolvedName.charAt(0).toUpperCase() + resolvedName.slice(1)); } if (!ctor && !isUuid && !isURL) { - if (!component.startsWith('cc.')) { - // 添加 'cc.' 查询 - const fullName = 'cc.' + component; - ctor = cc.js.getClassByName(fullName); + if (!resolvedName.startsWith('cc.')) { + ctor = cc.js.getClassByName('cc.' + resolvedName); if (!ctor && !isStartWithUppercase) { - // 添加 cc. 并且后面首字母大写 - const fullName = 'cc.' + component.charAt(0).toUpperCase() + component.slice(1); - ctor = cc.js.getClassByName(fullName); + ctor = cc.js.getClassByName('cc.' + resolvedName.charAt(0).toUpperCase() + resolvedName.slice(1)); } - } else if (component.length > 3 && component.charAt(3) != component.charAt(0).toUpperCase()) { - // 如果是 cc.lalel 直接更换为 cc.Label 查询 - const fullName = component.slice(0, 3) + component.at(3)?.toUpperCase() + component.slice(4); - ctor = cc.js.getClassByName(fullName); + } else if (resolvedName.length > 3 && resolvedName.charAt(3) !== resolvedName.charAt(3).toUpperCase()) { + ctor = cc.js.getClassByName(resolvedName.slice(0, 3) + resolvedName.charAt(3).toUpperCase() + resolvedName.slice(4)); } } } + if (!ctor) { - console.error(`ctor with name ${component} is not found `); if (isUuid) { - throw new Error(`Target Component('${component}') Not Found. Hint: Please use the correct component uuid`); + throw new Error(`Target Component('${resolvedName}') Not Found. Hint: Please use the correct component uuid`); } else if (isURL) { - throw new Error(`Target Component('${component}') Not Found. Hint: Please use the correct component url`); + throw new Error(`Target Component('${resolvedName}') Not Found. Hint: Please use the correct component url`); } else { - throw new Error(`Target Component('${component}') Not Found. Hint: Please use the correct component name`); + throw new Error(`Target Component('${resolvedName}') Not Found. Hint: Please use the correct component name`); } } - if (cc.js.isChildClassOf(ctor, Component)) { - comp = node.addComponent(ctor as Constructor); // 触发引擎上节点添加组件 - } else { - console.error(`ctor with name ${component} is not child class of Component `); + if (!cc.js.isChildClassOf(ctor, Component)) { throw new Error(`Constructor has been found, but it is not component-based.`); } - this.emit('component:add', comp); - - return dumpUtil.dumpComponent(comp as Component); + return ctor as Constructor; } - async addComponent(params: IAddComponentOptions): Promise { + async add(params: IAddComponentOptions): Promise { try { await Service.Editor.lock(); - return await this.addComponentImpl(params.nodePathOrUuid, params.component); - } catch (error) { - console.error(error); - throw error; - } finally { - Service.Editor.unlock(); - } - } + if (Array.isArray(params.component)) { + let lastDump: IComponent | null = null; + for (const id of params.component) { + lastDump = await this.add({ nodePath: params.nodePath, component: id }); + } + return lastDump!; + } - /** - * 创建组件 - * @param params - */ - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - private requireComponentList: Function[] = []; + const node = NodeMgr.getNodeByPath(params.nodePath); + if (!node) { + throw new Error(`create component failed: ${params.nodePath} does not exist`); + } + if (!params.component || params.component.length <= 0) { + throw new Error(`create component failed: component name cannot be empty`); + } - async createComponent(params: IAddComponentOptions): Promise { - if (Array.isArray(params.component)) { - params.component.forEach((id) => { - this.createComponent({ nodePathOrUuid: params.nodePathOrUuid, component: id }); - }); - console.warn('don\'t add component to more than one node at one time'); - return false; - } - const node = NodeMgr.getNodeByPath(params.nodePathOrUuid) ?? NodeMgr.getNode(params.nodePathOrUuid); - if (!node) { - console.warn(`create component failed: ${params.nodePathOrUuid} does not exist`); - return false; - } + const ctor = await this.resolveComponentCtor(params.component); - if (params.component) { - // 发送节点修改消息 this.emit('node:before-change', node); this.emit('component:before-add-component', params.component, node); - let comp = null; - try { - // 需要单独处理 missing script - if (params.component === 'MissingScript' || params.component === 'cc.MissingScript') { - throw new Error('Reset Component failed: MissingScript does not exist'); + // 处理 requireComponent 依赖链 + let iterateObj = ctor as any; + if (iterateObj._requireComponent) { + while (iterateObj._requireComponent) { + this.requireComponentList.push(iterateObj._requireComponent); + iterateObj = iterateObj._requireComponent; } + } - /** - * 增加编辑器对外 create-component 接口的兼容性 - * getClassById(string) 查不到的时候,再查一次 getClassByName(string) - */ - let ctor = cc.js.getClassById(params.component); - if (!ctor) { - ctor = cc.js.getClassByName(params.component); - } - if (cc.js.isChildClassOf(ctor, Component)) { - let iterateObj = ctor as any; - if (iterateObj._requireComponent) { - while (iterateObj._requireComponent) { - this.requireComponentList.push(iterateObj._requireComponent); - iterateObj = iterateObj._requireComponent; - } - } - comp = node.addComponent(ctor as Constructor); // 触发引擎上节点添加组件 - this.requireComponentList = []; - } else { - console.error(`ctor with name ${params.component} is not child class of Component `); - } - const mode = this.queryMode(); - if (mode === 'prefab') { - // 理论上应该使用 - const rootNode = Service.Editor.getRootNode(); - if (rootNode && hasOneKindOfComponent(node, UITransform) && !hasOneKindOfComponent(rootNode, Canvas)) { - // 为了显示,节点结构为:scene node > canvas node > prefab root node - createShouldHideInHierarchyCanvasNode(director.getScene()!).then((target) => { - rootNode.parent = target; - }); - } + const comp = node.addComponent(ctor); + this.requireComponentList = []; + + // prefab 模式下的 Canvas 创建 + const mode = this.queryMode(); + if (mode === 'prefab') { + const rootNode = Service.Editor.getRootNode(); + if (rootNode && hasOneKindOfComponent(node, UITransform) && !hasOneKindOfComponent(rootNode, Canvas)) { + createShouldHideInHierarchyCanvasNode(director.getScene()!).then((target) => { + rootNode.parent = target; + }); } - this.checkComponentsCollision(node); - this.checkDynamicBodyShape(node); - } catch (error) { - console.error(error); - } - if (comp) { - compMgr.onComponentAddedFromEditor(comp); } - // 发送节点修改消息 + this.checkComponentsCollision(node); + this.checkDynamicBodyShape(node); + + this.emit('component:add', comp); + compMgr.onComponentAddedFromEditor(comp); this.emit('node:change', node, { type: NodeEventType.CREATE_COMPONENT }); - } else { - console.warn(`create component failed: ${params.component} does not exist`); - return false; - } - return true; + const dump = await translateDumpI18n(dumpUtil.dumpComponent(comp as Component)) as IComponent; + // hack: 以下字段不属于编辑器 dump 结构(IComponent),仅用于 proxy 层将复杂的 dump 转换为 CLI 所需的扁平结构 + (dump as any).__component_path__ = compMgr.getPathFromUuid(comp.uuid) ?? ''; + (dump as any).__compPrefab__ = (comp as any).__prefab || null; + return dump; + } catch (error) { + console.error(error); + throw error; + } finally { + Service.Editor.unlock(); + } } @@ -368,7 +299,7 @@ export class ComponentService extends BaseService implements I } } - async removeComponent(params: IRemoveComponentOptions): Promise { + async remove(params: IRemoveComponentOptions): Promise { try { await Service.Editor.lock(); @@ -377,7 +308,7 @@ export class ComponentService extends BaseService implements I throw new Error(`Remove component failed: ${params.path} does not exist`); } - this.emit('component:before-remove', comp); + this.emit('component:before-remove-component', comp); const result = compMgr.removeComponent(comp); // 需要立刻执行removeComponent操作,否则会延迟到下一帧 cc.Object._deferredDestroy(); @@ -392,67 +323,33 @@ export class ComponentService extends BaseService implements I } } - async queryComponentImpl(params: IQueryComponentOptions, isEditor: boolean = false): Promise { + async queryImpl(params: IQueryComponentOptions): Promise { const comp = await this.findComponent(params.path); if (!comp) { console.warn(`Query component failed: ${params.path} does not exist`); return null; } - if (isEditor) { - return (dumpUtil.dumpComponentForEditor(comp as Component)); - } else { - return (dumpUtil.dumpComponent(comp as Component)); - } + const dump = await translateDumpI18n(dumpUtil.dumpComponent(comp as Component)) as IComponent; + // hack: 以下字段不属于编辑器 dump 结构(IComponent),仅用于 proxy 层将复杂的 dump 转换为 CLI 所需的扁平结构 + (dump as any).__component_path__ = compMgr.getPathFromUuid(comp.uuid) ?? ''; + (dump as any).__compPrefab__ = (comp as any).__prefab || null; + return dump; } - async queryComponent(params: IQueryComponentOptions | string): Promise { + async query(params: IQueryComponentOptions | string): Promise { if (typeof params === 'string') { - return this.queryComponentImpl({ path: params }, true); + return this.queryImpl({ path: params }); } else { - return this.queryComponentImpl(params); + return this.queryImpl(params); } } - async setPropertyForCli(options: ISetPropertyOptions): Promise { - try { - await Service.Editor.lock(); - return this.setPropertyImp(options); - } catch (error) { - console.error(error); - throw error; - } finally { - Service.Editor.unlock(); - } - } - - async setProperty(options: ISetPropertyOptions | ISetPropertyOptionsForEditor): Promise { - if ('uuid' in options) { - return await this.setPropertyForEditor(options as ISetPropertyOptionsForEditor); - } else { - return await this.setPropertyForCli(options); - } - } - - /** - * 查询一个节点的实例 - * @param {*} uuid - * @return {cc.Node} - */ - query(uuid: string | undefined): Node | null { - if (typeof uuid === 'undefined') { - return null; - } - // TODO(qgh): nodeMgr应该添加queryRecycleNode - // return NodeMgr.getNode(uuid) ?? NodeMgr.queryRecycleNode(uuid); - return NodeMgr.getNode(uuid); - } - - async setPropertyForEditor(options: ISetPropertyOptionsForEditor): Promise { + async setProperty(options: ISetPropertyOptions): Promise { // 多个节点更新值 - if (Array.isArray(options.uuid)) { + if (Array.isArray(options.nodePath)) { try { - for (let i = 0; i < options.uuid.length; i++) { - await this.setPropertyForEditor({ uuid: options.uuid[i], path: options.path, dump: options.dump, record: options?.record }); + for (let i = 0; i < options.nodePath.length; i++) { + await this.setProperty({ nodePath: options.nodePath[i], path: options.path, dump: options.dump, record: options?.record }); } return true; } catch (e) { @@ -460,9 +357,9 @@ export class ComponentService extends BaseService implements I return false; } } - const node = this.query(options.uuid); + const node = NodeMgr.getNodeByPath(options.nodePath); if (!node) { - console.warn(`Set property failed: ${options.uuid} does not exist`); + console.warn(`Set property failed: ${options.nodePath} does not exist`); return false; } @@ -497,34 +394,21 @@ export class ComponentService extends BaseService implements I return true; } - private async setPropertyImp(options: ISetPropertyOptions): Promise { - const component = compMgr.queryFromPath(options.componentPath); - if (!component) { - throw new Error(`Failed to set property: Target component(${options.componentPath}) not found`); - } - const compProperties = (dumpUtil.dumpComponent(component as Component)); - const properties = Object.entries(options.properties); - - const idx = component.node.components.findIndex(comp => comp === component); - for (const [key, value] of properties) { - if (!compProperties.properties[key]) { - throw new Error(`Failed to set property: Target property(${key}) not found`); - // continue; - } - const compProperty = compProperties.properties[key]; - compProperty.value = value; - // 恢复数据 - await dumpUtil.restoreProperty(component, key, compProperty); - - this.emit('component:set-property', component, { - type: NodeEventType.SET_PROPERTY, - propPath: `__comps__.${idx}.${key}`, - }); + /** + * 查询一个节点的实例 + * @param {*} uuid + * @return {cc.Node} + */ + queryNode(uuid: string | undefined): Node | null { + if (typeof uuid === 'undefined') { + return null; } - return true; + // TODO(qgh): nodeMgr应该添加queryRecycleNode + // return NodeMgr.getNode(uuid) ?? NodeMgr.queryRecycleNode(uuid); + return NodeMgr.getNode(uuid); } - async queryAllComponent(): Promise { + async queryAll(): Promise { const keys = Object.keys(cc.js._registeredClassNames); const components: string[] = []; keys.forEach((key) => { @@ -538,7 +422,7 @@ export class ComponentService extends BaseService implements I return components; } - async queryComponentHasScript(name: string): Promise { + async hasScript(name: string): Promise { const classes = await this.queryClasses(); return classes.some((cls) => cls.name === name); } @@ -574,8 +458,8 @@ export class ComponentService extends BaseService implements I return classes; } - async queryComponentFunctionOfNode(uuid: string): Promise { - const node = NodeMgr.getNode(uuid); + async queryFunctionOfNode(path: string): Promise { + const node = NodeMgr.getNodeByPath(path); if (!node) { return {}; } @@ -587,8 +471,8 @@ export class ComponentService extends BaseService implements I } private readonly CompMgrEventHandlers = { - ['add']: 'add', - ['remove']: 'remove', + ['add']: 'onCompAdd', + ['remove']: 'onCompRemove', } as const; private compMgrEventHandlers = new Map void>(); /** @@ -618,7 +502,7 @@ export class ComponentService extends BaseService implements I * @param {String} uuid * @param {cc.Component} component */ - add(uuid: string, component: Component) { + onCompAdd(uuid: string, component: Component) { if (isEditorNode(component.node)) { return; } @@ -630,7 +514,7 @@ export class ComponentService extends BaseService implements I * @param {String} uuid * @param {cc.Component} component */ - remove(uuid: string, component: Component) { + onCompRemove(uuid: string, component: Component) { if (isEditorNode(component.node)) { return; } @@ -641,7 +525,7 @@ export class ComponentService extends BaseService implements I * 重置组件 * @param uuid component 的 uuid */ - public async resetComponent(params: IQueryComponentOptions): Promise { + public async reset(params: IQueryComponentOptions): Promise { try { const comp = await this.findComponent(params.path); if (!comp) { @@ -662,7 +546,11 @@ export class ComponentService extends BaseService implements I } } - public async executeComponentMethod(options: IExecuteComponentMethodOptions): Promise { - return await compMgr.executeComponentMethod(options.uuid, options.name, options.args); + public async executeMethod(options: IExecuteComponentMethodOptions): Promise { + const comp = compMgr.queryFromPath(options.path); + if (!comp) { + return null; + } + return await compMgr.executeComponentMethod(comp.uuid, options.name, options.args); } } diff --git a/src/core/scene/scene-process/service/component/index.ts b/src/core/scene/scene-process/service/component/index.ts index 8a8df9d81..6b978c78a 100644 --- a/src/core/scene/scene-process/service/component/index.ts +++ b/src/core/scene/scene-process/service/component/index.ts @@ -1,14 +1,22 @@ -import { EventEmitter } from 'events'; -import dumpUtil from '../dump'; -const { get } = require('lodash'); +import dumpUtil, { translateDumpI18n } from '../dump'; +import get from 'lodash/get'; const CompMgr = EditorExtends.Component; import utils from './utils'; import { Component, MissingScript } from 'cc'; import { IProperty } from '../../../@types/public'; -import { IComponentIdentifier } from '../../../common'; +import { type IComponentEvents } from '../../../common'; +import { ServiceEvents } from '../core/global-events'; + +export class CompManager { + protected _recycleComponent: Record = {}; + + emit(event: K, ...args: IComponentEvents[K]): void; + emit(event: string, ...args: any[]): void; + emit(event: string, ...args: any[]) { + ServiceEvents.emit(event, ...args); + } -export class CompManager extends EventEmitter { init() { this.registerCompMgrEvents(); } @@ -49,7 +57,7 @@ export class CompManager extends EventEmitter { * @param {cc.Component} component */ add(uuid: string, component: Component) { - this.emit('added', component); + this.emit('component:added', component); } /** @@ -58,7 +66,7 @@ export class CompManager extends EventEmitter { * @param {cc.Component} component */ remove(uuid: string, component: Component) { - this.emit('removed', component); + this.emit('component:removed', component); } /** @@ -76,22 +84,29 @@ export class CompManager extends EventEmitter { return CompMgr.getComponentFromPath(path) || null; } - getComponentIdentifier(component: Component): IComponentIdentifier { - const path = this.getPathFromUuid(component.uuid); - return { - cid: (component as any).__cid__, - type: cc.js.getClassName(component.constructor), - uuid: component.uuid, - name: component.name, - enabled: component.enabled ? true : false,//enalbed maybe undefined. - path: path === null ? '' : path, - }; - } - getPathFromUuid(uuid: string): string | null { return CompMgr.getPathFromUuid(uuid); } + addRecycleComponent(uuid: string) { + if (this._recycleComponent[uuid]) { + delete this._recycleComponent[uuid]; + } + } + + removeRecycleComponent(uuid: string, comp: Component) { + this._recycleComponent[comp.uuid] = comp; + } + + /** + * 在回收站中查询一个组件的实例 + * @param {*} uuid + * @returns {cc.Component} + */ + queryRecycle(uuid: string): Component | null { + return this._recycleComponent[uuid] ?? null; + } + /** * 获取所有在用的组件 */ @@ -112,12 +127,12 @@ export class CompManager extends EventEmitter { return false; } - this.emit('before-remove-component', component); + this.emit('component:before-remove-component', component); component.node.removeComponent(component); // 需要立刻执行removeComponent操作,否则会延迟到下一帧 cc.Object._deferredDestroy(); - this.emit('remove', component); + this.emit('component:remove', component); return true; } @@ -150,7 +165,7 @@ export class CompManager extends EventEmitter { try { const node = new cc.Node(); const newComp = node.addComponent(component.constructor); - const dump = dumpUtil.dumpComponentForEditor(newComp); + const dump = dumpUtil.dumpComponent(newComp); for (const key in dump.value) { if (skipCompProps.includes(key)) { @@ -174,12 +189,12 @@ export class CompManager extends EventEmitter { * 如果组件不存在,则返回 null * @param {String} uuid */ - queryDump(uuid: string) { + async queryDump(uuid: string) { const comp = this.query(uuid); if (!comp) { return null; } - return dumpUtil.dumpComponentForEditor(comp); + return translateDumpI18n(dumpUtil.dumpComponent(comp)); } /** @@ -237,7 +252,7 @@ export class CompManager extends EventEmitter { return; } - this.emit('add', component); + this.emit('component:add', component); // 一些组件在添加的时候,需要执行部分特殊的逻辑 if (component.constructor && (utils.addComponentMap as any)[component.constructor.name]) { diff --git a/src/core/scene/scene-process/service/core/base-service.ts b/src/core/scene/scene-process/service/core/base-service.ts index 4a7860be5..f0c4cf44d 100644 --- a/src/core/scene/scene-process/service/core/base-service.ts +++ b/src/core/scene/scene-process/service/core/base-service.ts @@ -26,10 +26,9 @@ export interface IServiceEvents { onSetPropertyComponent?(comp: Component): void; onComponentAdded?(comp: Component): void; onComponentRemoved?(comp: Component): void; + onBeforeChangeComponent?(node: Node): void; + onBeforeAddComponent?(name:string, node: Node): void; onBeforeRemoveComponent?(comp: Component): void; - onComponentBeforeChanged?(node: Node): void; - onBeforeComponentAdded?(name:string, node: Node): void; - onComponentChanged?(name:string, opts: IChangeNodeOptions): void; // Asset events onAssetDeleted?(uuid: string): void; diff --git a/src/core/scene/scene-process/service/dump/decode.ts b/src/core/scene/scene-process/service/dump/decode.ts index aeb2d03ce..4a26c3fd4 100644 --- a/src/core/scene/scene-process/service/dump/decode.ts +++ b/src/core/scene/scene-process/service/dump/decode.ts @@ -2,14 +2,70 @@ declare const cc: any; -import { ccClassAttrPropertyDefaultValue, getDefault, getTypeInheritanceChain, parsingPath } from './utils'; +import { ccClassAttrPropertyDefaultValue, getDefault, getTypeInheritanceChain, getTypeName, parsingPath } from './utils'; -import lodash from 'lodash'; -const { get, set } = lodash; +import get from 'lodash/get'; +import set from 'lodash/set'; import { DumpDefines } from './dump-defines'; -import { Component, editorExtrasTag, Node, Vec3, MobilityMode } from 'cc'; -import { openSync } from 'fs'; -const NodeMgr = EditorExtends.Node; +import { Component, editorExtrasTag, Node, Vec3, MobilityMode, Prefab, Quat, assetManager, Animation } from 'cc'; +import { promisify } from 'util'; +import { IComponent, INode, IScene, ITargetOverrideInfo } from '../../../common'; +import compMgr from './../component/index'; +import nodeMgr from './../node/index'; +import { IProperty } from '../../../@types/public'; + +type TargetOverrideInfo = Prefab._utils.TargetOverrideInfo; +const TargetOverrideInfo = Prefab._utils.TargetOverrideInfo; +type TargetInfo = Prefab._utils.TargetInfo; +const TargetInfo = Prefab._utils.TargetInfo; +type PrefabInfo = Prefab._utils.PrefabInfo; +const PrefabInfo = Prefab._utils.PrefabInfo; + +function decodeChildren(children: any[], node: any) { + const dumpChildrenUuids: string[] = children.map((child: any) => child.value.uuid); + const nodeChildrenUuids: string[] = node.children.map((child: INode) => child.uuid); + + /** + * 出于性能考虑,不去移动两个数组共有的节点 + * 移除在 node 中且不在 dump 中的 uuid + * 添加在 dump 中且不在 node 中的 uuid + * 按照 dump 中的顺序重新排列 + */ + nodeChildrenUuids.forEach((uuid: string) => { + // 删除不存在的节点 + if (!dumpChildrenUuids.includes(uuid)) { + const child = nodeMgr.query(uuid); + // 重要:过滤隐藏节点 或 无效节点 + if (!child || child.objFlags & cc.Object.Flags.HideInHierarchy) { + return; + } + child.parent = null; + } + }); + + dumpChildrenUuids.forEach((uuid: string, i: number) => { + const child = nodeMgr.query(uuid); + // 重要:过滤无效节点 + if (!child) { + return; + } + + // 重置对象状态位,后续应该提供还原的方法 + child.walk((node: Node) => { + node._objFlags &= cc.Object.Flags.PersistentMask; + node._objFlags &= (~cc.Object.Flags.Destroyed); + }); + + // 节点挂靠父级 + if (!nodeChildrenUuids.includes(uuid)) { + child.parent = node; + } + + // 按新的顺序排列 + child.setSiblingIndex(i); + }); +} + // 还原mountedRoot export function decodeMountedRoot(compOrNode: Node | Component, mountedRoot?: string) { @@ -19,7 +75,7 @@ export function decodeMountedRoot(compOrNode: Node | Component, mountedRoot?: st if (typeof mountedRoot === 'undefined') { return null; } - const mountedRootNode = NodeMgr.getNode(mountedRoot); + const mountedRootNode = nodeMgr.query(mountedRoot); if (mountedRootNode) { if (!compOrNode[editorExtrasTag]) { compOrNode[editorExtrasTag] = {}; @@ -32,6 +88,305 @@ export function decodeMountedRoot(compOrNode: Node | Component, mountedRoot?: st } } +// 差异还原节点上的组件 +async function decodeComponents(dumpComps: any, node: Node, excludeComps?: any) { + if (!dumpComps) { + // 容错处理 + return; + } + + // 用于判断 prefabNode 下的 component 复用 + const prefabFileIdToDumpComp: { [key: string]: any } = {}; + const dumpCompsUuids = dumpComps + .map((comp: any) => { + if (comp.value.uuid) { + if (comp.value.__prefab && comp.value.__prefab.value && comp.value.__prefab.value.fileId.value) { + prefabFileIdToDumpComp[comp.value.__prefab.value.fileId.value] = comp; + } + + return comp.value.uuid.value; + } + return ''; + }) + .filter(Boolean); + + const componentsUuids = node.components + .map((component: any) => { + if (excludeComps) { + // 需要 exclude 的 component,假装不在 node 上 + const compType = getTypeName(component.constructor); + if (excludeComps.includes(compType)) { + return ''; + } + } + + // 将 dumpComp 转为现有相同 fileId component 的配置,后面执行值覆盖 + if (component.__prefab && component.__prefab.fileId) { + const dumpComp = prefabFileIdToDumpComp[component.__prefab.fileId]; + if (dumpComp) { + const existIndex = dumpCompsUuids.indexOf(dumpComp.value.uuid.value); + if (existIndex !== -1) { + dumpCompsUuids.splice(existIndex, 1, component.uuid); + dumpComp.value.uuid.value = component.uuid; + } + } + } + + return component.uuid; + }) + .filter(Boolean); + + /** + * 删除现有在 node._compoennts 中但不在 dumpComps 中的 component + * 2次方: 次数限制的作用: + * 既能再次删除被依赖而不能被先删除的组件, + * 又能避免死循环 + */ + let maxLoopTimes = componentsUuids.length ** 2; + let i = componentsUuids.length - 1; + + do { + const compUuid = componentsUuids[i]; + + if (compUuid && !dumpCompsUuids.includes(compUuid)) { + const comp = compMgr.query(compUuid); + // 删除失败会返回 false, 可能是组件被依赖,会下次再删 + if (!comp || compMgr.removeComponent(comp)) { + componentsUuids.splice(i, 1); + } else { + i--; + } + } else { + i--; + } + + maxLoopTimes--; + } while (componentsUuids.length !== 0 && maxLoopTimes); + + // 重要:当前帧执行删除,保障下面的排序逻辑和上面的删除处于同一帧 + cc.Object._deferredDestroy(); + + // 挂载上新的组件及调整组件的位置 + const components = node.components.slice(); // 下一步会清空,先缓存一份,以用于比较 + node['_components'].length = 0; // 先清空节点上的组件 + + for (let i = 0; i < dumpComps.length; i++) { + const dumpComp: IComponent = dumpComps[i]; + + if (!dumpComp.value || !dumpComp.value.uuid) { + continue; + } + + let component = components[i]; + + const compUuid = (dumpComp.value.uuid as IProperty).value as string; + let cacheComp = compMgr.query(compUuid); + + // 在用,查询没有 + if (!cacheComp) { + // 从 回收站 再查出来 + cacheComp = compMgr.queryRecycle(compUuid); + } + + if (cacheComp) { + // 有缓存 + if (component !== cacheComp) { + /** + * 新增场景:组件是从别的节点移过来的, + * 例如 prefab 从资源还原时,会先实例化一个临时节点,里面的组件会被移植过来 + */ + if (cacheComp.node !== node) { + _removeDependComponent(cacheComp); + } + + // 组件已被删除 + if (cacheComp.objFlags & cc.Object.Flags.Destroying || cacheComp.objFlags & cc.Object.Flags.Destroyed) { + // 57349 , 5 不会等于 128 + // 重置 component.objFlags 的状态是为了重新走组件的生命周期 + cacheComp.objFlags &= cc.Object.Flags.PersistentMask; + cacheComp.objFlags &= ~cc.Object.Flags.Destroyed; + + // 回收站的缓存机制是编辑器的,这里需要将组件从回收站还原 + // cce.Component.recycle(compUuid); + } + component = cacheComp; + } + nodeMgr.addComponentAt(node, component, i); // 插入新位置 + } + + // 编辑器预览时,undo时会设置clips导致动画停止播放 #15236 + // 记录上次播放的动画(因为可能不是默认clip),还原后再播放 + const playAnim: string[] = []; + // TODO(qgh):判断是否为预览进程 + // if (isPreviewProcess && dumpComp.type === 'cc.Animation') { + // const anim = component as Animation; + // anim.clips.map((clip) => anim.getState(clip?.name ?? '')) + // .filter((state: AnimationState) => state?.isPlaying) + // .forEach((state: AnimationState) => { playAnim.push(state.name); }); + // } + // 对于原先还在的组件,还原内部的值 + for (const key in dumpComp.value) { + await decodePatch(key, dumpComp.value[key], component); + } + + if (playAnim.length > 0) { + const anim = component as Animation; + playAnim.forEach((name: string) => { + anim.play(name); + }); + } + + // 还原mountedRoot + decodeMountedRoot(component, dumpComp.mountedRoot); + + // TODO: 不知道为啥这个方法是个protected的,应该改成public的 + // @ts-ignore + if (component && component.onRestore) { + // @ts-ignore + component.onRestore(); + } + } + + // 按依赖关系的顺序删除组件 + function _removeDependComponent(component: any) { + // 组件已被删除 + if (component.objFlags & cc.Object.Flags.Destroying || component.objFlags & cc.Object.Flags.Destroyed) { + // 57349 , 5 不会等于 128 + return; + } + + // 关系是 dependComponent 依赖 component + const dependComponent = component.node._getDependComponent(component); + dependComponent.forEach((dep: any) => { + _removeDependComponent(dep); + }); + + /** + * 需要立即执行 cc.Object._deferredDestroy() 动作 + */ + compMgr.removeComponent(component); + cc.Object._deferredDestroy(); + } +} + + +async function decodePrefab(dumpPrefab: any, node: any) { + // 不需要处理 + if (!dumpPrefab && !node['_prefab']) { + return; + } + + // 删除 + if (!dumpPrefab && node['_prefab']) { + node['_prefab'] = null; + return; + } + + // 新增 + const info = new PrefabInfo(); + const root = nodeMgr.query(dumpPrefab.rootUuid); + info.root = root ? root : node; + if (dumpPrefab.uuid) { + try { + info.asset = await promisify(assetManager.loadAny)(dumpPrefab.uuid); + } catch (e) { + console.error(e); + info.asset = new Prefab(); + info.asset.initDefault(dumpPrefab.uuid); + } + } + info.fileId = dumpPrefab.fileId || node.uuid; + if (dumpPrefab.instance) { + await decodePatch('instance', dumpPrefab.instance, info); + } else { + info.instance = undefined; + } + + if (dumpPrefab.targetOverrides) { + info.targetOverrides = decodeTargetOverrides(dumpPrefab.targetOverrides); + } else { + info.targetOverrides = undefined; + } + + node['_prefab'] = info; +} + +/** + * 解码一个场景 dump 数据 + * @param dump + * @param scene + */ +export async function decodeScene(dump: IScene, scene?: any) { + if (!dump) { + return; + } + scene = scene || new cc.Scene(); + scene.name = dump.name.value; + scene.active = dump.active.value; + if (dump.children) { + decodeChildren(dump.children, scene); + } + + for (const key of Object.keys(dump._globals)) { + await decodePatch(`_globals.${key}`, dump._globals[key], scene); + } + + if (dump.targetOverrides) { + if (!scene['_prefab']) { + scene['_prefab'] = new cc._PrefabInfo(); + } + scene['_prefab'].targetOverrides = decodeTargetOverrides(dump.targetOverrides); + } else { + scene['_prefab'] = undefined; + } +} + +/** + * 解码一个 dump 数据 + * @param dump + * @param node + */ +export async function decodeNode(dump: INode, node?: Node, excludeComps?: any) { + if (!dump) { + return null; + } + + node = node || new cc.Node(); + + if (!node) { + return null; + } + + // 先还原prefab的相关信息,因为下面的属性设置会触发prefab的override + await decodePrefab(dump.__prefab__, node); + + node.name = dump.name.value as string; + node.active = dump.active.value as boolean; + node.layer = dump.layer.value as number; + node.mobility = dump.mobility.value as number; + node.setPosition(dump.position.value as Vec3); + const quat = new Quat(); + const vec3 = dump.rotation.value as Vec3; + Quat.fromEuler(quat, vec3.x, vec3.y, vec3.z); + node.setRotation(quat); + node.setScale(dump.scale.value as Vec3); + + decodeMountedRoot(node, dump.mountedRoot); + + if (dump.parent && dump.parent.value && dump.parent.value.uuid) { + node.parent = nodeMgr.query(dump.parent.value.uuid); + } else { + node.parent = null; + } + if (dump.children) { + decodeChildren(dump.children, node); + } + + await decodeComponents(dump.__comps__, node, excludeComps); + + return node; +} + async function _decodeByType(type: string, node: any, info: any, dump: any, opts?: any) { const dumpType = DumpDefines[type]; @@ -322,9 +677,38 @@ export function updatePropertyFromNull(node: any, path: string) { } } +export function decodeTargetOverrides(dumpedTargetOverrides: ITargetOverrideInfo[]) { + const targetOverrides: TargetOverrideInfo[] = []; + dumpedTargetOverrides.forEach((itr: ITargetOverrideInfo) => { + const targetOverride = new TargetOverrideInfo(); + targetOverride.source = nodeMgr.query(itr.source); + if (itr.sourceInfo) { + const sourceInfo = new TargetInfo(); + sourceInfo.localID = itr.sourceInfo; + targetOverride.sourceInfo = sourceInfo; + } + + targetOverride.propertyPath = itr.propertyPath; + + targetOverride.target = nodeMgr.query(itr.target); + if (itr.targetInfo) { + const targetInfo = new TargetInfo(); + targetInfo.localID = itr.targetInfo; + targetOverride.targetInfo = targetInfo; + } + + targetOverrides.push(targetOverride); + }); + + return targetOverrides; +} + export default { + decodeScene, + decodeNode, decodePatch, resetProperty, updatePropertyFromNull, decodeMountedRoot, + decodeTargetOverrides, }; diff --git a/src/core/scene/scene-process/service/dump/encode.ts b/src/core/scene/scene-process/service/dump/encode.ts index 5c922c53e..f91b70e4a 100644 --- a/src/core/scene/scene-process/service/dump/encode.ts +++ b/src/core/scene/scene-process/service/dump/encode.ts @@ -7,69 +7,267 @@ import dumpUtil from './utils'; import { DumpDefines } from './dump-defines'; import { IProperty } from '../../../@types/public'; -import { IComponent, IComponentForEditor } from '../../../common'; +import { IComponent, INode, IPrefab, IScene, ITargetOverrideInfo } from '../../../common'; import compMgr from '../component/index'; import { prefabUtils } from './../prefab/utils'; import { Service } from './../core'; +import { MobilityMode, Node, Prefab, Component, js } from 'cc'; + +const attributeProps = [ + 'enumList', + 'radioGroup', + 'bitmaskList', + 'displayName', + 'group', + 'multiline', + 'step', + 'slide', + 'tooltip', + 'animatable', + 'unit', + 'radian', + 'displayOrder', +]; + +const autoI18nAttributeNames = [ + 'displayName', + 'tooltip', +] as const; + +export function encodePrefab(node: Node): IPrefab | null { + if (!node['_prefab']) return null; + const prefabStateInfo = prefabUtils.getPrefabStateInfo(node); + const rootNode = node['_prefab'].root; + const result: IPrefab = { + uuid: (node['_prefab'].asset && node['_prefab'].asset._uuid) || '', + fileId: node['_prefab'].fileId, + rootUuid: rootNode?.uuid || '', + sync: true, + prefabStateInfo, + }; + if (node['_prefab'].targetOverrides) { + result.targetOverrides = encodeTargetOverrides(node['_prefab'].targetOverrides) ?? undefined; + } + if (node['_prefab'].instance) { + result.instance = encodeObject(node['_prefab'].instance, { default: null }, node); + } + return result; +} /** - * 编码一个 component - * @param component + * 编码一个 node 数据 + * @param node */ -export function encodeComponent(component: any): IComponent { - const ctor = component.constructor; +export function encodeNode(node: Node): INode { + const ctor = node.constructor; - const data: IComponent = { - properties: {}, - path: compMgr.getPathFromUuid(component.uuid) || '', - uuid: component.uuid, - name: component.name, - enabled: component.enabled, - type: dumpUtil.getTypeName(ctor), - cid: component.__cid__, - prefab: component.__prefab - }; + const LayersEnumList = Object.keys(cc.Layers.Enum).map((key, index) => { + return { name: key, value: cc.Layers.Enum[key] }; + }); + LayersEnumList.sort((a, b) => { + return a.value - b.value; + }); - // 遍历组件内所有属性 - ctor.__props__.forEach((key: string) => { - try { - if (key in component) { - /** - * 过滤私有属性与内置属性 - */ - if (key.startsWith('_') || key.startsWith('__')) { + const MobilityModeEnumList = Object.keys(MobilityMode).map((key, index) => { + return { name: key, value: MobilityMode[key as keyof typeof MobilityMode] }; + }); + + // FIXME: avoid using private field + // TODO:这里的需要知道当前场景是 2D 还是 3D + //const is2DProject = cce.SceneFacadeManager['_projectType'] === '2d'; + const is2DProject = false; + + const data: INode = { + path: EditorExtends.Node.getNodePath(node), + active: encodeObject(node.active, { displayName: 'Active', default: null }, node), + locked: encodeObject(Boolean(node.objFlags & cc.Object.Flags.LockedInEditor), { displayName: 'Locked', default: false, animatable: false }, node), + name: encodeObject(node.name, { displayName: 'Name', default: null, animatable: false }, node), + position: encodeObject( + node.position, + { + displayName: 'i18n:scene.cc.Node.properties.position.displayName', + default: new cc.math.Vec3(), + tooltip: 'i18n:scene.cc.Node.properties.position.tooltip', + }, + node, + 'position', + ), + rotation: encodeObject( + node.eulerAngles, + { + name: 'eulerAngles', + displayName: 'i18n:scene.cc.Node.properties.eulerAngles.displayName', + default: new cc.math.Vec3(), + tooltip: `i18n:scene.cc.Node.properties.eulerAngles.${is2DProject ? 'tooltip2D' : 'tooltip3D'}`, + }, + node, + is2DProject ? 'angle' : 'eulerAngles', + ), + scale: encodeObject( + node.scale, + { + displayName: 'i18n:scene.cc.Node.properties.scale.displayName', + default: new cc.math.Vec3(1, 1, 1), + tooltip: 'i18n:scene.cc.Node.properties.scale.tooltip', + }, + node, + 'scale', + ), + mobility: encodeObject( + node.mobility, + { + displayName: 'i18n:scene.cc.Node.properties.mobility.displayName', + tooltip: 'i18n:scene.cc.Node.properties.mobility.tooltip', + default: 0, + type: 'Enum', + enumList: MobilityModeEnumList, + }, + node, + 'mobility', + ), + layer: encodeObject( + node.layer, { + displayName: 'i18n:scene.cc.Node.properties.layer.displayName', + tooltip: 'i18n:scene.cc.Node.properties.layer.tooltip', + default: 1073741824, + type: 'Enum', + enumList: LayersEnumList, + readonly: false, + animatable: false, + }, + node, + 'layer', + ), + uuid: encodeObject(node.uuid, { displayName: 'UUID', default: null, animatable: false }, node), + + parent: encodeObject( + node.parent, + { + ctor: cc.Node, + }, + node, + ), + + children: node.children + .map((child: any) => { + if (!child || child.objFlags & cc.Object.Flags.HideInHierarchy) { return; } - /** - * 此处 cc.Class.attr(component, key) 中的 component 不能用 ctor 替代 - * 因为 ctor 是基类定义,component 是子类,子类的 __attr__ 存了一些自己数据了 - * 比如 sp.Skeleton 当 skeletonData 属性有数据时取 _animationIndex 属性的 enumList 数据 - */ - const attrs = cc.Class.attr(component, key); - const dumpData = encodeObject(component[key], attrs, component, key); - if (dumpData.type !== 'Unknown') { - data.properties[key] = dumpData; + + return encodeObject( + child, + { + ctor: cc.Node, + }, + node, + ); + }) + .filter((v): v is IProperty => !!v), + + __type__: dumpUtil.getTypeName(ctor), + __comps__: node['_components'].map((comp: any) => { + return encodeComponent(comp); + }), + + mountedRoot: prefabUtils.getMountedRoot(node)?.uuid, + }; + + if (node['_prefab']) { + data.__prefab__ = encodePrefab(node)!; + + const removedComponents = prefabUtils.getRemovedComponents(node); + if (removedComponents.length > 0) { + data.removedComponents = removedComponents.map((comp: Component) => { + return { name: js.getClassName(comp), fileID: comp.__prefab!.fileId }; + }); + } + } + + // 根据 flag 调整 readyonly + _checkObjFlags(node, data); + + // 填充 path,供 inspector setProperty 使用 + for (const [key, val] of Object.entries(data)) { + if (val && typeof val === 'object' && !Array.isArray(val) && 'type' in val && 'value' in val) { + (val as IProperty).path = key; + } + } + data.__comps__.forEach((comp, index) => { + if (comp.value && typeof comp.value === 'object' && !Array.isArray(comp.value)) { + for (const [key, prop] of Object.entries(comp.value as Record)) { + if (prop && typeof prop === 'object' && !Array.isArray(prop) && 'type' in prop && 'value' in prop) { + (prop as IProperty).path = `__comps__.${index}.${key}`; } - _checkConstructorRewriteType(dumpData, component[key], attrs); } - } catch (error) { - // tslint:disable-next-line:max-line-length - console.warn( - `Component property dump failed:\n Node: ${component.node.name}(${component.node.uuid})\n Component: ${data.type}(${component.uuid})\n Property: ${key}`, - ); - console.warn(error); - delete data.properties[key]; } }); return data; } +/** + * 编码一个场景数据 + * @param scene + */ +export function encodeScene(scene: any): IScene { + const ctor = scene.constructor; + + const data: IScene = { + path: '/', + active: encodeObject(scene.active, { default: null }), + locked: encodeObject(false, { default: false }), + name: encodeObject(scene.name || ctor.name, { default: null }), + uuid: encodeObject(scene.uuid, { default: null }), + autoReleaseAssets: encodeObject(scene.autoReleaseAssets, { displayName: 'Auto Release Assets', default: false }), + children: scene.children + .map((child: any) => { + if (!child || child.objFlags & cc.Object.Flags.HideInHierarchy) { + return; + } + + return encodeObject(child, { + ctor: cc.Node, + }); + }) + .filter((v: any): v is IProperty => !!v), + parent: '', + __type__: dumpUtil.getTypeName(ctor), + _globals: {}, + isScene: true, + }; + + // 遍历 scene._globals 内所有属性 + if (scene._globals) { + scene._globals.constructor.__props__.map((key: string) => { + const attrs = cc.Class.attr(scene._globals.constructor, key); + data._globals[key] = encodeObject(scene._globals[key], attrs, scene._globals); + }); + } + + if (scene['_prefab']?.targetOverrides) { + data.targetOverrides = encodeTargetOverrides(scene['_prefab'].targetOverrides) ?? undefined; + } + + // 填充 path,供 inspector setProperty 使用 + for (const [key, val] of Object.entries(data)) { + if (val && typeof val === 'object' && !Array.isArray(val) && 'type' in val && 'value' in val) { + (val as IProperty).path = key; + } + } + for (const [key, val] of Object.entries(data._globals)) { + if (val && typeof val === 'object' && 'type' in val && 'value' in val) { + (val as IProperty).path = `_globals.${key}`; + } + } + + return data; +} + /** * 详细的编码 component * @param component */ -export function encodeComponentForEditor(component: any): IComponentForEditor { +export function encodeComponent(component: any): IComponent { const ctor = component.constructor; // 嵌套预制体中的mountedComponent并不是mounted;需要做区分 const mountedRootNode = prefabUtils.getMountedRoot(component); @@ -84,12 +282,13 @@ export function encodeComponentForEditor(component: any): IComponentForEditor { } } } - const data: IComponentForEditor = { + const data: IComponent = { value: { uuid: encodeObject(component.uuid, { default: null, visible: false }, component), name: encodeObject(component.name, { default: null, visible: false }, component), enabled: encodeObject(component.enabled, { default: null, visible: false }, component), }, + path: compMgr.getPathFromUuid(component.uuid) ?? 'unknown', default: undefined, type: dumpUtil.getTypeName(ctor), readonly: false, @@ -180,7 +379,37 @@ function _checkConstructorRewriteType(data: IProperty, object: any, attributes: } } -function _checkAttributes(data: IProperty, attributes: any) { +function _checkFuncAttribute(attributeName: string, attributes: any, owner: any): any { + const attribute = attributes[attributeName]; + if (attribute === undefined) return; + + if (typeof attribute === 'function') { + if (!owner) { + console.warn(`try to use ${attributeName} function without owner`); + } else { + const value = attribute.call(owner); + if (typeof value === 'boolean') { + return !!value; + } + return value; + } + } else if (typeof attribute === 'boolean') { + return !!attribute; + } else { + return attribute; + } +} + +function _checkAttributes(data: IProperty, attributes: any, owner: any) { + // 处理存在函数写法的属性 + ['visible', 'min', 'max'].forEach((name: string) => { + const attributeName = name as keyof IProperty; + const value = _checkFuncAttribute(attributeName, attributes, owner); + if (value !== undefined) { + data[attributeName] = value; + } + }); + if (!attributes.ctor && attributes.type) { data.type = '' + attributes.type; } @@ -193,6 +422,58 @@ function _checkAttributes(data: IProperty, attributes: any) { if (attributes && attributes.hasGetter && !attributes.hasSetter) { data.readonly = true; } + + attributeProps.forEach((propName) => { + // eslint-disable-next-line no-prototype-builtins + if (attributes.hasOwnProperty(propName)) { + // @ts-ignore + data[propName] = attributes[propName]; + } + }); + + // 如果对象类型名以 `cc.` 开始,也就是引擎对象。 + // 则自动按规则组装出要 i18n 的特性(比如显示名和工具提示)的 i18n 路径,作为 Dump 数据。 + // + // 组装规则如下。对于某个引擎类的某个属性的某个特性,编辑器会按以下的字典路径去查找该特性的 i18n 字符串: + // `i18n:ENGINE.classes.<类的 cc-class 名称>.properties.<属性的名称>.<特性的名称>` + // + if (typeof data.name === 'string' && owner && typeof owner === 'object') { + const ownerTypeName = findClassName(owner, data.name); + if (ownerTypeName) { + for (const autoI18nAttributeName of autoI18nAttributeNames) { + // 如果该特性已经被声明,比如 `@property({ tooltip: '' })`,跳过组装。 + if (Object.prototype.hasOwnProperty.call(attributes, autoI18nAttributeName)) { + continue; + } + data[autoI18nAttributeName] = `i18n:ENGINE.classes.${ownerTypeName}.properties.${data.name}.${autoI18nAttributeName}`; + } + } + } +} + +/** + * 查询指定类名,如果自身没有就向上查询 + * @param ccClassObject + */ +const MAX_RECURSION_DEPTH = 10;// 递归中增加最大递归深度限制,避免无限循环或性能问题 +const TARGET_CLASS_NAME = ['cc.', 'sp.']; +function findClassName(ccClassObject: any, property: string): string { + let depth = 0; + let proto = ccClassObject; + while (proto && depth < MAX_RECURSION_DEPTH) { + const className = js.getClassName(proto); + + if (className && + TARGET_CLASS_NAME.find(key => className.startsWith(key)) && + Object.prototype.hasOwnProperty.call(proto, property)) { + return className; + } + // 通过原型链向上查找 + proto = Object.getPrototypeOf(proto); + depth++; + } + + return ''; } function _encodeByType(type: string | undefined, object: any, data: IProperty, opts?: any) { @@ -206,6 +487,71 @@ function _encodeByType(type: string | undefined, object: any, data: IProperty, o return false; } +/** + * hack:处理 component 的 .objFlags 设置,需要传递给 node + * 比如 Canvas 的 IsPositionLocked 要传给 node,position.readonly = true + * 比如 Canvas 的 IsSizeLocked 要传给 UITransform, contentsize = true + * 暂时处理以下逻辑,后续可增删 + */ +function _checkObjFlags(node: any, data: INode) { + let IsPositionLocked = false; + let IsSizeLocked = false; + let IsAnchorLocked = false; + let IsScaleLocked = false; + let IsRotationLocked = false; + node['_components'].forEach((component: any) => { + if (component.objFlags & cc.Object.Flags.IsPositionLocked) { + IsPositionLocked = true; + } + + if (component.objFlags & cc.Object.Flags.IsSizeLocked) { + IsSizeLocked = true; + } + + if (component.objFlags & cc.Object.Flags.IsAnchorLocked) { + IsAnchorLocked = true; + } + + if (component.objFlags & cc.Object.Flags.IsScaleLocked) { + IsScaleLocked = true; + } + + if (component.objFlags & cc.Object.Flags.IsRotationLocked) { + IsRotationLocked = true; + } + }); + + if (IsPositionLocked) { + data.position.readonly = true; + } + if (IsScaleLocked) { + data.scale.readonly = true; + } + + if (IsRotationLocked) { + data.rotation.readonly = true; + } + + const uiTransformComponents: any = []; + data.__comps__.forEach((comp: any) => { + if (comp.cid === 'cc.UITransform') { + uiTransformComponents.push(comp); + } + }); + + if (uiTransformComponents.length) { + if (IsSizeLocked) { + uiTransformComponents.forEach((comp: any) => { + comp.value.contentSize.readonly = true; + }); + } + if (IsAnchorLocked) { + uiTransformComponents.forEach((comp: any) => { + comp.value.anchorPoint.readonly = true; + }); + } + } +} /** * 编码一个对象 @@ -214,9 +560,25 @@ function _encodeByType(type: string | undefined, object: any, data: IProperty, o * @param owner 编码对象所属的对象 * @param objectKey 输出有效信息,当前数据 key,以便问题排查 */ -export function encodeObject(object: any, attributes: any, owner: any = null, objectKey?: string): IProperty { +export function encodeObject(object: any, attributes: any, owner: any = null, objectKey?: string, isTemplate?: boolean): IProperty { const ctor = dumpUtil.getConstructor(object, attributes); - // let defValue = dumpUtil.getDefault(attributes); + let defValue = dumpUtil.getDefault(attributes); + + // 构造器存在,属性也存在 + if (defValue && typeof defValue === 'object' && defValue.constructor && Array.isArray(defValue.constructor.__props__)) { + const result: { [key: string]: any } = { + type: dumpUtil.getTypeName(defValue.constructor), + value: {}, + }; + defValue.constructor.__props__.forEach((key: string) => { + const attrs = cc.Class.attr(defValue.constructor, key); + const dumpData = encodeObject(defValue[key], attrs, defValue, key); + if (dumpData.type !== 'Unknown') { + result.value[key] = dumpData; + } + }); + defValue = result; + } let type = dumpUtil.getTypeName(ctor); @@ -234,7 +596,12 @@ export function encodeObject(object: any, attributes: any, owner: any = null, ob const data: IProperty = { name: objectKey, value: null, + default: defValue, type: type, + path: '', + readonly: !!attributes.readonly, + visible: true, + animatable: attributes.animatable === undefined ? true : !!attributes.animatable, // 如果没有定义默认是 true,否则根据定义取布尔值 }; //如果有 userData 就把 userData 传递过去 @@ -242,7 +609,13 @@ export function encodeObject(object: any, attributes: any, owner: any = null, ob data.userData = attributes.userData; } - _checkAttributes(data, attributes); + _checkAttributes(data, attributes, owner); + + if (defValue) { + if (Array.isArray(defValue)) { + data.isArray = true; + } + } if (!data.isArray && Array.isArray(object)) { data.isArray = true; @@ -265,6 +638,10 @@ export function encodeObject(object: any, attributes: any, owner: any = null, ob // 子元素的类型由父级决定,子元素的默认值跟随父级类型的默认值 childAttribute.default = getElementDefaultValue(attributes, propertyDefaultValue); + if (!isTemplate) { + data.elementTypeData = encodeObject(childAttribute.default, childAttribute, propertyDefaultValue, undefined, true); + } + const resultValue: any = []; // 未避免有可能出现的内部数据有空,需要用普通的 for 循环,不要使用 forEach\map 等来遍历 for (let i = 0; i < object.length; i++) { @@ -277,6 +654,8 @@ export function encodeObject(object: any, attributes: any, owner: any = null, ob const result = encodeObject(item, childAttribute, owner); if (result.type !== 'Unknown') { resultValue.push(result); + } else { + resultValue.push(data.elementTypeData); } } data.value = resultValue; @@ -305,8 +684,8 @@ export function encodeObject(object: any, attributes: any, owner: any = null, ob const result: { [key: string]: any } = {}; ctor.__props__.forEach((key: string) => { const attrs = cc.Class.attr(object, key); // object 是实例,可能有自定义的 attrs - - if (attributes.readonly && attributes.readonly.deep) { + + if (attributes.readonly && attributes.readonly.deep){ attrs.readonly = { deep: true }; } @@ -329,6 +708,11 @@ export function encodeObject(object: any, attributes: any, owner: any = null, ob } } + // 继承链 + if (ctor) { + data.extends = dumpUtil.getTypeInheritanceChain(ctor); + } + return data; } @@ -354,8 +738,34 @@ function getElementDefaultValueFromParentInitializer(parentInitializer: unknown) return null; } +function encodeTargetOverrides(targetOverrides: any) { + if (!targetOverrides || targetOverrides.length <= 0) { + return null; + } + + const dumpedTargetOverrides: ITargetOverrideInfo[] = []; + targetOverrides.forEach((itr: Prefab._utils.TargetOverrideInfo) => { + if (!itr.source || !itr.target) { + return; + } + const dumpOverride = { + source: itr.source.uuid, + sourceInfo: itr.sourceInfo ? itr.sourceInfo.localID : undefined, + propertyPath: itr.propertyPath, + target: itr.target.uuid, + targetInfo: itr.targetInfo ? itr.targetInfo.localID : undefined, + }; + + dumpedTargetOverrides.push(dumpOverride); + }); + + return dumpedTargetOverrides; +} + +// export * as default from './encode'; export default { + encodeNode, + encodeScene, encodeComponent, - encodeComponentForEditor, encodeObject, }; diff --git a/src/core/scene/scene-process/service/dump/index.ts b/src/core/scene/scene-process/service/dump/index.ts index c40b1845a..e7564acf4 100644 --- a/src/core/scene/scene-process/service/dump/index.ts +++ b/src/core/scene/scene-process/service/dump/index.ts @@ -1,13 +1,12 @@ 'use strict'; -import { Node, Component, js, CCClass } from 'cc'; +import { Node, Component, js, CCClass, Scene } from 'cc'; import { parsingPath } from './utils'; +import get from 'lodash/get'; import AssetUtil from './asset'; -import { decodePatch, resetProperty, updatePropertyFromNull } from './decode'; -import { encodeObject, encodeComponent, encodeComponentForEditor } from './encode'; -import { IComponent, IComponentForEditor } from '../../../common'; - -// import * as dumpDecode from './decode'; -const { get } = require('lodash'); +import { decodePatch, decodeNode, decodeScene, resetProperty, updatePropertyFromNull } from './decode'; +import { encodeObject, encodeComponent, encodeScene, encodeNode } from './encode'; +import { IComponent, INode, IScene } from '../../../common'; +import { Rpc } from '../../rpc'; // dump接口,统一下全局引用 class DumpUtil { @@ -25,24 +24,29 @@ class DumpUtil { return ret; } - // 生成一个component的dump数据 - dumpComponent(comp: Component): IComponent; - dumpComponent(comp: null | undefined): null; - dumpComponent(comp: Component | null | undefined) { - if (!comp) { + /** + * 生成一个 node 的 dump 数据 + * @param {*} node + */ + dumpNode(node: Node): INode | IScene | null { + if (!node) { return null; } - return encodeComponent(comp); + if (node instanceof Scene) { + return encodeScene(node); + } + return encodeNode(node); + } // 生成一个component的dump数据 - dumpComponentForEditor(comp: Component): IComponentForEditor; - dumpComponentForEditor(comp: null | undefined): null; - dumpComponentForEditor(comp: Component | null | undefined) { + dumpComponent(comp: Component): IComponent; + dumpComponent(comp: null | undefined): null; + dumpComponent(comp: Component | null | undefined) { if (!comp) { return null; } - return encodeComponentForEditor(comp); + return encodeComponent(comp); } /** @@ -84,6 +88,18 @@ class DumpUtil { return updatePropertyFromNull(node, path); } + /** + * 还原一个节点的全部属性 + * @param {*} node + * @param {*} dump + */ + async restoreNode(node: Node, dump: any) { + if (dump && dump.isScene) { + return await decodeScene(dump, node); + } + return await decodeNode(dump, node); + } + /** * 解析节点的访问路径 * @param path @@ -119,4 +135,70 @@ class DumpUtil { } +function collectI18nKeys(obj: any, keys: Set) { + if (!obj || typeof obj !== 'object') return; + if (typeof obj.displayName === 'string' && obj.displayName.startsWith('i18n:')) { + keys.add(obj.displayName); + } + if (typeof obj.tooltip === 'string' && obj.tooltip.startsWith('i18n:')) { + keys.add(obj.tooltip); + } + if (obj.value && typeof obj.value === 'object') { + if (Array.isArray(obj.value)) { + for (const item of obj.value) { + collectI18nKeys(item, keys); + } + } else { + for (const key in obj.value) { + collectI18nKeys(obj.value[key], keys); + } + } + } + if (Array.isArray(obj.__comps__)) { + for (const comp of obj.__comps__) { + collectI18nKeys(comp, keys); + } + } +} + +function applyI18nTranslations(obj: any, translations: Record) { + if (!obj || typeof obj !== 'object') return; + if (typeof obj.displayName === 'string' && translations[obj.displayName] !== undefined) { + obj.displayName = translations[obj.displayName]; + } + if (typeof obj.tooltip === 'string' && translations[obj.tooltip] !== undefined) { + obj.tooltip = translations[obj.tooltip]; + } + if (obj.value && typeof obj.value === 'object') { + if (Array.isArray(obj.value)) { + for (const item of obj.value) { + applyI18nTranslations(item, translations); + } + } else { + for (const key in obj.value) { + applyI18nTranslations(obj.value[key], translations); + } + } + } + if (Array.isArray(obj.__comps__)) { + for (const comp of obj.__comps__) { + applyI18nTranslations(comp, translations); + } + } +} + +export async function translateDumpI18n(dump: T): Promise { + if (!dump) return dump; + const keys = new Set(); + collectI18nKeys(dump, keys); + if (keys.size === 0) return dump; + try { + const translations = await Rpc.getInstance().request('i18n', 'batchTransI18nName', [Array.from(keys)]); + applyI18nTranslations(dump, translations); + } catch (e) { + console.warn('[Dump] Failed to translate i18n keys via RPC:', e); + } + return dump; +} + export default new DumpUtil(); diff --git a/src/core/scene/scene-process/service/editor.ts b/src/core/scene/scene-process/service/editor.ts index 3cc3eff65..a01eec6fc 100644 --- a/src/core/scene/scene-process/service/editor.ts +++ b/src/core/scene/scene-process/service/editor.ts @@ -6,12 +6,11 @@ import { ICreateOptions, IEditorEvents, IEditorService, - INode, IOpenOptions, IReloadOptions, ISaveOptions, - IScene, ReloadResult, + TEditorEntity, } from '../../common'; import { PrefabEditor, SceneEditor } from './editors'; import { IAssetInfo } from '../../../assets/@types/public'; @@ -24,8 +23,8 @@ import { Rpc } from '../rpc'; @register('Editor') export class EditorService extends BaseService implements IEditorService { private needReloadAgain: IReloadOptions | null = null; - private lastSceneOrNode: IScene | INode | undefined; - private reloadPromise: Promise | null = null; + private lastSceneOrNode: TEditorEntity | undefined; + private reloadPromise: Promise | null = null; private currentEditorUuid: string | null = null; // 当前打开的编辑器 UUID private editorMap: Map = new Map(); // uuid -> editor @@ -97,7 +96,7 @@ export class EditorService extends BaseService implements IEditor } } - async queryCurrent(): Promise { + async queryCurrent(): Promise { const editor = this.currentEditorUuid && this.editorMap.get(this.currentEditorUuid); console.log(`current editor: ${this.currentEditorUuid} `); return editor ? await editor.encode() : null; @@ -108,8 +107,8 @@ export class EditorService extends BaseService implements IEditor return editor ? editor.getRootNode() : null; } - async open(params: IOpenOptions): Promise { - const { urlOrUUID, simpleNode = true } = params; + async open(params: IOpenOptions): Promise { + const { urlOrUUID } = params; const assetInfo = await Rpc.getInstance().request('assetManager', 'queryAssetInfo', [urlOrUUID]); if (!assetInfo) { @@ -156,7 +155,7 @@ export class EditorService extends BaseService implements IEditor editor = this.createEditor(assetInfo.type); this.editorMap.set(uuid, editor); } - const encode = await editor.open(assetInfo, simpleNode); + const encode = await editor.open(assetInfo); // 设置当前打开的编辑器 this.currentEditorUuid = assetInfo.uuid; this.emit('editor:open'); diff --git a/src/core/scene/scene-process/service/editors/base-editor.ts b/src/core/scene/scene-process/service/editors/base-editor.ts index 7c1d8c767..b9bef8215 100644 --- a/src/core/scene/scene-process/service/editors/base-editor.ts +++ b/src/core/scene/scene-process/service/editors/base-editor.ts @@ -105,8 +105,8 @@ export abstract class BaseEditor { } // 抽象方法,子类必须实现 - abstract encode(simpleNode?: boolean, entity?: IEditorTarget): Promise; - abstract open(asset: IAssetInfo, simpleNode?: boolean): Promise; + abstract encode(entity?: IEditorTarget): Promise; + abstract open(asset: IAssetInfo): Promise; abstract close(): Promise; abstract save(): Promise; /** diff --git a/src/core/scene/scene-process/service/editors/prefab-editor.ts b/src/core/scene/scene-process/service/editors/prefab-editor.ts index 2e72c56b5..c1443c7b8 100644 --- a/src/core/scene/scene-process/service/editors/prefab-editor.ts +++ b/src/core/scene/scene-process/service/editors/prefab-editor.ts @@ -15,16 +15,15 @@ export class PrefabEditor extends BaseEditor { private virtualScene: Scene | null = null; - async encode(simpleNode?: boolean, entity?: IEditorTarget | null): Promise { + async encode(entity?: IEditorTarget | null): Promise { entity = entity ?? this.entity; if (!entity) { throw new Error('encode 失败,没有打开预制体'); } - simpleNode = simpleNode ?? true; - return sceneUtils.generateNodeInfo(entity.instance, !simpleNode); + return await sceneUtils.generateNodeDump(entity.instance) as INode; } - async open(asset: IAssetInfo, simpleNode?: boolean): Promise { + async open(asset: IAssetInfo): Promise { // 获取预制体标识符 const identifier = this.getIdentifier(asset); // 加载预制体资源 @@ -41,7 +40,7 @@ export class PrefabEditor extends BaseEditor { instance }); - return this.encode(simpleNode); + return this.encode(); } async close(): Promise { diff --git a/src/core/scene/scene-process/service/editors/scene-editor.ts b/src/core/scene/scene-process/service/editors/scene-editor.ts index 9a877a5c0..af79d9d60 100644 --- a/src/core/scene/scene-process/service/editors/scene-editor.ts +++ b/src/core/scene/scene-process/service/editors/scene-editor.ts @@ -1,5 +1,5 @@ import { Scene, SceneAsset, Component, Node } from 'cc'; -import { type IBaseIdentifier, ICreateOptions, IEditorTarget, INode, IScene } from '../../../common'; +import { type IBaseIdentifier, ICreateOptions, IEditorTarget, IScene } from '../../../common'; import { Rpc } from '../../rpc'; import { sceneUtils } from '../scene/utils'; import { BaseEditor } from './base-editor'; @@ -13,37 +13,22 @@ import { editorPrefabUtils } from '../prefab/prefab-editor-utils'; */ export class SceneEditor extends BaseEditor { - async encode(simpleNode?: boolean, entity?: IEditorTarget | null): Promise { + async encode(entity?: IEditorTarget | null): Promise { entity = entity ?? this.entity; if (!entity) { throw new Error('encode 失败,没有打开场景'); } - simpleNode = simpleNode ?? true; - return { - ...entity.identifier, - name: entity.instance.name, - prefab: sceneUtils.generatePrefabInfo(entity.instance['_prefab']), - children: entity.instance.children - .map((node: Node) => { - if(simpleNode) { - return sceneUtils.generateNodeIdentifier(node); - } else { - return sceneUtils.generateNodeInfo(node, true); - } - }) - .filter(child => child !== null) as INode[], - components: entity.instance.components - .map((component: Component) => { - return sceneUtils.generateComponentInfo(component); - }) - }; + const scene = await sceneUtils.generateNodeDump(entity.instance) as IScene; + const d = scene as any; + d.__identifier__ = entity.identifier; + return scene; } - async open(asset: IAssetInfo, simpleNode?: boolean): Promise { + async open(asset: IAssetInfo): Promise { const identifier = this.getIdentifier(asset); if (this.entity?.identifier.assetUuid === identifier.assetUuid) { - return await this.encode(simpleNode); + return await this.encode(); } const sceneAsset = await sceneUtils.loadAny(identifier.assetUuid); @@ -54,7 +39,7 @@ export class SceneEditor extends BaseEditor { identifier, }); - return this.encode(simpleNode); + return this.encode(); } async close(): Promise { diff --git a/src/core/scene/scene-process/service/node.ts b/src/core/scene/scene-process/service/node.ts index cbb2ba131..e6e3dacf9 100644 --- a/src/core/scene/scene-process/service/node.ts +++ b/src/core/scene/scene-process/service/node.ts @@ -14,15 +14,16 @@ import { type IUpdateNodeResult, NodeType, NodeEventType, - EventSourceType, - IChangeNodeOptions + ISetPropertyOptions } from '../../common'; +import { type IScene } from '../../common/editor/scene'; import { Rpc } from '../rpc'; -import { CCClass, CCObject, Node, Prefab, Quat, Vec3, TransformBit, UITransform, LODGroup } from 'cc'; +import { CCClass, CCObject, Node, Prefab, Quat, Vec3 } from 'cc'; import { createNodeByAsset, loadAny } from './node/node-create'; -import { getUICanvasNode, isEditorNode, setLayer } from './node/node-utils'; -import { sceneUtils } from './scene/utils'; +import { getUICanvasNode, setLayer } from './node/node-utils'; import { prefabUtils } from './prefab/utils'; +import { sceneUtils } from './scene/utils'; +import nodeMgr from './node/index'; import NodeConfig from './node/node-type-config'; const NodeMgr = EditorExtends.Node; @@ -33,7 +34,7 @@ const NodeMgr = EditorExtends.Node; */ @register('Node') export class NodeService extends BaseService implements INodeService { - async createNodeByType(params: ICreateByNodeTypeParams): Promise { + async createByType(params: ICreateByNodeTypeParams): Promise { try { await Service.Editor.lock(); let canvasNeeded = params.canvasRequired || false; @@ -59,7 +60,7 @@ export class NodeService extends BaseService implements INodeServic } } - async createNodeByAsset(params: ICreateByAssetParams): Promise { + async createByAsset(params: ICreateByAssetParams): Promise { try { await Service.Editor.lock(); const assetUuid = await Rpc.getInstance().request('assetManager', 'queryUUID', [params.dbURL]); @@ -145,7 +146,7 @@ export class NodeService extends BaseService implements INodeServic resultNode.name = name; } if (checkUITransform) { - this.ensureUITransformComponent(resultNode); + nodeMgr.ensureUITransformComponent(resultNode); } // 发送添加节点事件,添加节点中的根节点 @@ -156,7 +157,7 @@ export class NodeService extends BaseService implements INodeServic this.emit('node:change', parent, { type: NodeEventType.CHILD_CHANGED }); } - return sceneUtils.generateNodeInfo(resultNode, true); + return sceneUtils.generateNodeDump(resultNode) as Promise; } /** @@ -216,7 +217,7 @@ export class NodeService extends BaseService implements INodeServic // 设置父级 nextNode.setParent(currentParent); // 确保新创建的节点有必要的组件 - this.ensureUITransformComponent(nextNode); + nodeMgr.ensureUITransformComponent(nextNode); // 发送节点创建事件 this.emit('node:add', nextNode); @@ -231,7 +232,7 @@ export class NodeService extends BaseService implements INodeServic return currentParent; } - async deleteNode(params: IDeleteNodeParams): Promise { + async delete(params: IDeleteNodeParams): Promise { try { await Service.Editor.lock(); const root = Service.Editor.getRootNode(); @@ -245,26 +246,7 @@ export class NodeService extends BaseService implements INodeServic return null; } - // 发送节点修改消息 - const parent = node.parent; - this.emit('node:before-remove', node); - if (parent) { - this.emit('node:before-change', parent); - } - - node.setParent(null, params.keepWorldTransform); - node._objFlags |= CCObject.Flags.Destroyed; - // 3.6.1 特殊 hack,请在后续版本移除 - // 相关修复 pr: https://github.com/cocos/cocos-editor/pull/890 - try { - this._walkNode(node, (child: any) => { - child._objFlags |= CCObject.Flags.Destroyed; - }); - } catch (error) { - console.warn(error); - } - - this.emit('node:remove', node); + nodeMgr.baseRemoveNode(node, params.keepWorldTransform); return { path: path, @@ -277,14 +259,7 @@ export class NodeService extends BaseService implements INodeServic } } - private _walkNode(node: Node, func: Function) { - node && node.children && node.children.forEach((child) => { - func(child); - this._walkNode(child, func); - }); - } - - async updateNode(params: IUpdateNodeParams): Promise { + async update(params: IUpdateNodeParams): Promise { const updateOperate = () => { const node = NodeMgr.getNodeByPath(params.path); if (!node) { @@ -385,21 +360,17 @@ export class NodeService extends BaseService implements INodeServic } } - async queryNode(params: IQueryNodeParams): Promise { + async query(params?: IQueryNodeParams): Promise { try { await Service.Editor.lock(); const root = Service.Editor.getRootNode(); if (!root) { throw new Error('Failed to query node: the scene is not opened.'); } - let node = NodeMgr.getNodeByPath(params.path); - if (!params.path || params.path === '/') { - node = root; - } - if (!node) { - return null; - } - return sceneUtils.generateNodeInfo(node, !!params.queryChildren, !!params.queryComponent); + const path = params?.path; + const node = (path && path !== '/') ? NodeMgr.getNodeByPath(path) : root; + if (!node) return null; + return await sceneUtils.generateNodeDump(node); } catch (error) { console.error(error); throw error; @@ -467,35 +438,6 @@ export class NodeService extends BaseService implements INodeServic } } - /** - * 确保节点有 UITransform 组件 - * 目前只需保障在创建空节点的时候检查任意上级是否为 canvas - */ - ensureUITransformComponent(node: Node) { - if (node instanceof cc.Node && node.children.length === 0) { - // 空节点 - let inside = false; - let parent = node.parent; - - while (parent) { - const components = parent.components.map((comp) => cc.js.getClassName(comp.constructor)); - if (components.includes('cc.Canvas')) { - inside = true; - break; - } - parent = parent.parent; - } - - if (inside) { - try { - node.addComponent('cc.UITransform'); - } catch (error) { - console.error(error); - } - } - } - } - /** * 检查并根据需要创建 canvas节点或为父级添加UITransform组件,返回父级节点,如果需要canvas节点,则父级节点会是canvas节点 * @param workMode @@ -547,200 +489,77 @@ export class NodeService extends BaseService implements INodeServic const nodeMap = NodeMgr.getNodesInScene(); // 场景载入后要将现有节点监听所需事件 Object.keys(nodeMap).forEach((key) => { - this.registerEventListeners(nodeMap[key]); + nodeMgr.registerEventListeners(nodeMap[key]); }); - this.registerNodeMgrEvents(); + nodeMgr.registerNodeMgrEvents(); Service.Component.init(); } public onEditorClosed() { Service.Component.unregisterCompMgrEvents(); - this.unregisterNodeMgrEvents(); + nodeMgr.unregisterNodeMgrEvents(); const nodeMap = NodeMgr.getNodes(); Object.keys(nodeMap).forEach((key) => { - this.unregisterEventListeners(nodeMap[key]); + nodeMgr.unregisterEventListeners(nodeMap[key]); }); NodeMgr.clear(); EditorExtends.Component.clear(); } - // ---------- - - private readonly NodeHandlers = { - [Node.EventType.TRANSFORM_CHANGED]: 'onNodeTransformChanged', - [Node.EventType.SIZE_CHANGED]: 'onNodeSizeChanged', - [Node.EventType.ANCHOR_CHANGED]: 'onNodeAnchorChanged', - [Node.EventType.CHILD_ADDED]: 'onNodeParentChanged', - [Node.EventType.CHILD_REMOVED]: 'onNodeParentChanged', - [Node.EventType.LIGHT_PROBE_CHANGED]: 'onLightProbeChanged', - } as const; - private nodeHandlers = new Map(); - - /** - * 监听引擎发出的 node 事件 - * @param {*} node - */ - registerEventListeners(node: Node) { - if (!node || !node.isValid || isEditorNode(node)) { - return; + public async previewSetProperty(options: ISetPropertyOptions): Promise { + const node = NodeMgr.getNodeByPath(options.nodePath); + if (!node) { + return false; } - - // 遍历事件映射表,统一注册事件 - Object.entries(this.NodeHandlers).forEach(([eventType, handlerName]) => { - const boundHandler = (this as any)[handlerName].bind(this, node); - node.on(eventType, boundHandler, this); - this.nodeHandlers.set(`${eventType}_${node.uuid}`, boundHandler); - }); + return await nodeMgr.previewSetNodeProperty(node.uuid, options.path, options.dump); } - /** - * 取消监听引擎发出的node事件 - * @param {*} node - */ - unregisterEventListeners(node: Node) { - if (!node || !node.isValid || isEditorNode(node)) { - return; + public async cancelPreviewSetProperty(options: ISetPropertyOptions): Promise { + const node = NodeMgr.getNodeByPath(options.nodePath); + if (!node) { + return false; } - - // 遍历事件映射表,统一取消事件 - Object.keys(this.NodeHandlers).forEach(eventType => { - const key = `${eventType}_${node.uuid}`; - const handler = this.nodeHandlers.get(key); - if (handler) { - node.off(eventType, handler); - this.nodeHandlers.delete(key); - } - }); + return await nodeMgr.cancelPreviewSetNodeProperty(node.uuid, options.path); } - private readonly NodeMgrEventHandlers = { - ['add']: 'add', - ['change']: 'change', - ['remove']: 'remove', - } as const; - private nodeMgrEventHandlers = new Map void>(); - /** - * 注册引擎 Node 管理相关事件的监听 - */ - registerNodeMgrEvents() { - this.unregisterNodeMgrEvents(); - Object.entries(this.NodeMgrEventHandlers).forEach(([eventType, handlerName]) => { - const handler = (this as any)[handlerName].bind(this); - NodeMgr.on(eventType, handler); - this.nodeMgrEventHandlers.set(eventType, handler); - // console.log(`NodeMgr on ${eventType}`); - }); - } - - unregisterNodeMgrEvents() { - for (const eventType of this.nodeMgrEventHandlers.keys()) { - const handler = this.nodeMgrEventHandlers.get(eventType); - if (handler) { - NodeMgr.off(eventType, handler); - this.nodeMgrEventHandlers.delete(eventType); - // console.log(`NodeMgr off ${eventType}`); - } + public async setProperty(options: ISetPropertyOptions): Promise { + const node = NodeMgr.getNodeByPath(options.nodePath); + if (!node) { + return false; } + return await nodeMgr.setProperty(node.uuid, options.path, options.dump); } - onNodeTransformChanged(node: Node, transformBit: TransformBit) { - const changeOpts: IChangeNodeOptions = { type: NodeEventType.TRANSFORM_CHANGED, source: EventSourceType.ENGINE }; - - switch (transformBit) { - case Node.TransformBit.POSITION: - changeOpts.propPath = 'position'; - break; - case Node.TransformBit.ROTATION: - changeOpts.propPath = 'rotation'; - break; - case Node.TransformBit.SCALE: - changeOpts.propPath = 'scale'; - break; + public async reset(path: string): Promise { + const node = NodeMgr.getNodeByPath(path); + if (!node) { + return false; } - - this.emit('node:change', node, changeOpts); + return await nodeMgr.resetNode(node.uuid); } - onNodeSizeChanged(node: Node) { - const changeOpts: IChangeNodeOptions = { type: NodeEventType.SIZE_CHANGED, source: EventSourceType.ENGINE }; - const uiTransform = node.getComponent(UITransform); - if (uiTransform) { - const index = node.components.indexOf(uiTransform); - changeOpts.propPath = `_components.${index}.contentSize`; + public async resetProperty(options: ISetPropertyOptions): Promise { + const node = NodeMgr.getNodeByPath(options.nodePath); + if (!node) { + return false; } - this.emit('node:change', node, changeOpts); + return await nodeMgr.resetProperty(node.uuid, options.path); } - onNodeAnchorChanged(node: Node) { - const changeOpts: IChangeNodeOptions = { type: NodeEventType.ANCHOR_CHANGED, source: EventSourceType.ENGINE }; - const uiTransform = node.getComponent(UITransform); - if (uiTransform) { - const index = node.components.indexOf(uiTransform); - changeOpts.propPath = `_components.${index}.anchorPoint`; + public async updatePropertyFromNull(options: ISetPropertyOptions): Promise { + const node = NodeMgr.getNodeByPath(options.nodePath); + if (!node) { + return false; } - this.emit('node:change', node, changeOpts); + return await nodeMgr.updatePropertyFromNull(node.uuid, options.path); } - onNodeParentChanged(parent: Node, child: Node) { - if (isEditorNode(child)) { + public async setNodeAndChildrenLayer(options: ISetPropertyOptions): Promise { + const node = NodeMgr.getNodeByPath(options.nodePath); + if (!node) { return; } - - this.emit('node:change', parent, { type: NodeEventType.CHILD_CHANGED }); - - // 自身 parent = null 为删除,最后会有 deleted 消息,所以不需要再发 changed 消息 - if (child.parent) { - this.emit('node:change', child, { type: NodeEventType.PARENT_CHANGED }); - } - } - - onLightProbeChanged(node: Node) { - const changeOpts: IChangeNodeOptions = { type: NodeEventType.LIGHT_PROBE_CHANGED, source: EventSourceType.ENGINE }; - this.emit('node:change', node, changeOpts); - } - - /** - * 添加一个节点到管理器内 - * @param uuid - * @param {*} node - */ - add(uuid: string, node: Node) { - this.registerEventListeners(node); - - if (!isEditorNode(node)) { - this.emit('node:added', node); - } - } - - /** - * 一个节点被修改,由 EditorExtends.Node.emit('change') 触发 - * @param uuid - * @param node - */ - change(uuid: string, node: Node) { - if (!isEditorNode(node)) { - // 这里是因为 LOD 组件在挂到场景的时候,修改了自己的数据,但编辑器暂时无法知道修改了哪些数据 - // 所以针对 LOD 部分,增加了 propPath, prefab 才能正常修改 - let path = ''; - const lodGroup = node.getComponent(LODGroup); - if (lodGroup) { - const index = node.components.indexOf(lodGroup); - path = `__comps__.${index}`; - } - this.emit('node:change', node, { type: NodeEventType.SET_PROPERTY, propPath: path }); - } - } - - /** - * 从管理器内移除一个指定的节点 - * @param uuid - * @param {*} node - */ - remove(uuid: string, node: Node) { - this.unregisterEventListeners(node); - if (!isEditorNode(node)) { - this.emit('node:removed', node, { source: EventSourceType.ENGINE }); - } + return await nodeMgr.setNodeAndChildrenLayer(node.uuid, options.dump); } } diff --git a/src/core/scene/scene-process/service/node/index.ts b/src/core/scene/scene-process/service/node/index.ts new file mode 100644 index 000000000..ba7452fc2 --- /dev/null +++ b/src/core/scene/scene-process/service/node/index.ts @@ -0,0 +1,1834 @@ +'use strict'; + +import { IProperty } from '../../../@types/public'; + +/** + * 节点管理器 + * 负责管理当前打开场景的 uuid 与节点对应关系 + */ + +const NodeMgr = EditorExtends.Node; + +import get from 'lodash/get'; +import set from 'lodash/set'; +import { isEditorNode, getNodeName } from './node-utils'; +import { ServiceEvents } from '../core/global-events'; + +// const { promisify } = require('util'); +// const { basename, extname } = require('path'); +// import nodeUtil from '../../../utils/node'; +import dumpUtil, { translateDumpI18n } from '../dump'; + +// import getComponentFunctionOfNode from '../component/get-component-function-of-node'; +import { + Node, + director, + Component, + UITransform, + CCObject, + MissingScript, + LODGroup, + Prefab, +} from 'cc'; + +import { EventSourceType, NodeEventType, NodeOperationType } from '../public/event-enum'; +import { + type INodeEvents, + type INode, + IChangeNodeOptions, +} from '../../../common'; +import { type IScene } from '../../../common/editor/scene'; + + +import { loadAny } from './node-create'; +import compMgr from '../component/index'; + +const creatableAssetTypes = [ + 'cc.AnimationClip', + 'cc.AudioClip', + 'cc.BitmapFont', + 'cc.LabelAtlas', + 'cc.Mesh', + 'cc.ParticleAsset', + 'cc.Prefab', + 'cc.Script', + 'cc.SpriteFrame', + 'cc.TTFFont', + 'cc.TerrainAsset', + 'cc.TiledMapAsset', + 'cc.VideoClip', + 'dragonBones.DragonBonesAsset', + 'dragonBones.DragonBonesAtlasAsset', + 'sp.SkeletonData', +]; + +// 用于复制粘贴操作,暂存被复制节点的 clone 对象 +let stashInstants: any = null; + +/** + * 节点管理器 + * + * Events: + * node.on('before-change', (node) => {}); + * node.on('before-add', (node) => {}); + * node.on('before-remove', (node) => {}); + * node.on('change', (node) => {}); + * node.on('add', (node) => {}); + * node.on('remove', (node) => {}); + */ +export class NodeManager { + _onNodeAdded?: (...args: any[]) => void; + _onNodeChanged?: (...args: any[]) => void; + _onNodeRemoved?: (...args: any[]) => void; + _onTransformChanged?: (...args: any[]) => void; + _onSizeChanged?: (...args: any[]) => void; + _onAnchorChanged?: (...args: any[]) => void; + _onParentChanged?: (...args: any[]) => void; + _onLightProbeChanged?: (...args: any[]) => void; + + emit(event: K, ...args: INodeEvents[K]): void; + emit(event: string, ...args: any[]): void; + emit(event: string, ...args: any[]) { + ServiceEvents.emit(event, ...args); + } + + private _previewPropertysCache: Map> = new Map(); + get creatableAssetTypes() { + return creatableAssetTypes; + } + + init() { } + + /** + * 传入一个场景,将内部的节点全部缓存 + * @param {*} scene + */ + initWithScene(scene: any) { + if (!scene) { + return; + } + + const nodeMap = NodeMgr.getNodesInScene(); + + // 场景载入后要将现有节点监听所需事件 + Object.keys(nodeMap).forEach((key) => { + this.registerEventListeners(nodeMap[key]); + }); + + this.registerNodeMgrEvents(); + compMgr.init(); + + // 缓存预览设置的属性,用于还原预览前的设置 + this._previewPropertysCache = new Map(); + + this.emit('node:inited', this.queryUuids(), scene); + } + + public onSceneOpened(scene: any) { + this.initWithScene(scene); + } + + public onSceneClosed() { + compMgr.unregisterCompMgrEvents(); + this.unregisterNodeMgrEvents(); + this.clear(); + } + + + private readonly NodeMgrEventHandlers = { + ['add']: 'add', + ['change']: 'change', + ['remove']: 'remove', + } as const; + private nodeMgrEventHandlers = new Map void>(); + /** + * 注册引擎 Node 管理相关事件的监听 + */ + registerNodeMgrEvents() { + this.unregisterNodeMgrEvents(); + Object.entries(this.NodeMgrEventHandlers).forEach(([eventType, handlerName]) => { + const handler = (this as any)[handlerName].bind(this); + NodeMgr.on(eventType, handler); + this.nodeMgrEventHandlers.set(eventType, handler); + // console.log(`NodeMgr on ${eventType}`); + }); + } + + unregisterNodeMgrEvents() { + for (const eventType of this.nodeMgrEventHandlers.keys()) { + const handler = this.nodeMgrEventHandlers.get(eventType); + if (handler) { + NodeMgr.off(eventType, handler); + this.nodeMgrEventHandlers.delete(eventType); + // console.log(`NodeMgr off ${eventType}`); + } + } + } + + private readonly NodeHandlers = { + [Node.EventType.TRANSFORM_CHANGED]: 'onNodeTransformChanged', + [Node.EventType.SIZE_CHANGED]: 'onNodeSizeChanged', + [Node.EventType.ANCHOR_CHANGED]: 'onNodeAnchorChanged', + [Node.EventType.CHILD_ADDED]: 'onNodeParentChanged', + [Node.EventType.CHILD_REMOVED]: 'onNodeParentChanged', + [Node.EventType.LIGHT_PROBE_CHANGED]: 'onLightProbeChanged', + } as const; + private nodeHandlers = new Map(); + + /** + * 监听引擎发出的 node 事件 + * @param {*} node + */ + registerEventListeners(node: Node) { + if (!node || !node.isValid || isEditorNode(node)) { + return; + } + + // 遍历事件映射表,统一注册事件 + Object.entries(this.NodeHandlers).forEach(([eventType, handlerName]) => { + const boundHandler = (this as any)[handlerName].bind(this, node); + node.on(eventType, boundHandler, this); + this.nodeHandlers.set(`${eventType}_${node.uuid}`, boundHandler); + }); + } + + /** + * 取消监听引擎发出的node事件 + * @param {*} node + */ + unregisterEventListeners(node: Node) { + if (!node || !node.isValid || isEditorNode(node)) { + return; + } + + // 遍历事件映射表,统一取消事件 + Object.keys(this.NodeHandlers).forEach(eventType => { + const key = `${eventType}_${node.uuid}`; + const handler = this.nodeHandlers.get(key); + if (handler) { + node.off(eventType, handler); + this.nodeHandlers.delete(key); + } + }); + } + + onNodeTransformChanged(node: Node, transformBit: any) { + const changeOpts: IChangeNodeOptions = { type: NodeEventType.TRANSFORM_CHANGED, source: EventSourceType.ENGINE }; + + switch (transformBit) { + case Node.TransformBit.POSITION: + changeOpts.propPath = 'position'; + break; + case Node.TransformBit.ROTATION: + changeOpts.propPath = 'rotation'; + break; + case Node.TransformBit.SCALE: + changeOpts.propPath = 'scale'; + break; + } + + this.emit('node:change', node, changeOpts); + } + + onNodeSizeChanged(node: Node) { + const changeOpts: IChangeNodeOptions = { type: NodeEventType.SIZE_CHANGED, source: EventSourceType.ENGINE }; + const uiTransform = node.getComponent(UITransform); + if (uiTransform) { + const index = node.components.indexOf(uiTransform); + changeOpts.propPath = `_components.${index}.contentSize`; + } + this.emit('node:change', node, changeOpts); + } + + onNodeAnchorChanged(node: Node) { + const changeOpts: IChangeNodeOptions = { type: NodeEventType.ANCHOR_CHANGED, source: EventSourceType.ENGINE }; + const uiTransform = node.getComponent(UITransform); + if (uiTransform) { + const index = node.components.indexOf(uiTransform); + changeOpts.propPath = `_components.${index}.anchorPoint`; + } + this.emit('node:change', node, changeOpts); + } + + /** + * 监听引擎中节点 node.setParent(parent) 所发出来的事件 + * @param {*} parent + * @param {*} child + */ + onNodeParentChanged(parent: Node, child: Node) { + if (isEditorNode(child)) { + return; + } + + this.emit('node:change', parent, { type: NodeEventType.CHILD_CHANGED }); + + // 自身 parent = null 为删除,最后会有 deleted 消息,所以不需要再发 changed 消息 + if (child.parent) { + this.emit('node:change', child, { type: NodeEventType.PARENT_CHANGED }); + } + } + + /** + * 监听light-probe changed事件 + */ + onLightProbeChanged(node: Node) { + const changeOpts: IChangeNodeOptions = { type: NodeEventType.LIGHT_PROBE_CHANGED, source: EventSourceType.ENGINE }; + this.emit('node:change', node, changeOpts); + } + + /** + * 清空当前管理的节点 + */ + clear() { + const nodeMap = NodeMgr.getNodes(); + Object.keys(nodeMap).forEach((key) => { + this.unregisterEventListeners(nodeMap[key]); + }); + + NodeMgr.clear(); + compMgr.clear(); + } + + /** + * 添加一个节点到管理器内 + * @param {*} node + */ + add(uuid: string, node: Node) { + this.registerEventListeners(node); + + if (!isEditorNode(node)) { + this.emit('node:added', node); + } + } + + /** + * 一个节点被修改,由EditorExtends.Node.emit('change')触发 + * @param uuid + * @param node + */ + change(uuid: string, node: Node) { + if (!isEditorNode(node)) { + // 这里是因为 LOD 组件在挂到场景的时候,修改了自己的数据,但编辑器暂时无法知道修改了哪些数据 + // 所以针对 LOD 部分,增加了 propPath, prefab 才能正常修改 + let path = ''; + const lodGroup = node.getComponent(LODGroup); + if (lodGroup) { + const index = node.components.indexOf(lodGroup); + path = `__comps__.${index}`; + } + this.emit('node:change', node, { type: NodeOperationType.SET_PROPERTY, propPath: path }); + } + } + + /** + * 从管理器内移除一个指定的节点 + * @param {*} node + */ + remove(uuid: string, node: Node) { + this.unregisterEventListeners(node); + if (!isEditorNode(node)) { + this.emit('node:removed', node, { source: EventSourceType.ENGINE }); + } + } + + /** + * 查询一个节点的实例 + * @param {*} uuid + * @return {cc.Node} + */ + query(uuid: string | undefined): Node | null { + if (typeof uuid === 'undefined') { + return null; + } + return NodeMgr.getNode(uuid); + } + + /** + * 查询受管理的所有节点的 uuid 数组 + */ + queryUuids() { + const nodeMap = NodeMgr.getNodes(); + return Object.keys(nodeMap); + } + + /** + * 查询一个节点,并返回该节点的 dump 数据 + * 如果节点已被删除 parent = null,则返回 null + * @param {String} uuid + */ + async queryDump(uuid: string): Promise { + // 只查现有场景里的节点,不需要再查回收站里的节点 + const node = NodeMgr.getNodesInScene()[uuid]; + if (!node) { + return null; + } + return translateDumpI18n(dumpUtil.dumpNode(node)); + } + + /** + * 查询一个节点,并返回该节点的 dump 数据 + * 不论节点是否被删除 + * @param {String} uuid + */ + async queryDumpAtAll(uuid: string): Promise { + const node = this.query(uuid); + if (!node) { + return null; + } + return translateDumpI18n(dumpUtil.dumpNode(node)); + } + + // /** + // * 查询当前场景的节点树信息 + // * @param uuid asset uuid + // */ + // queryNodesByAssetUuid(uuid: string) { + // if (!uuid) { + // return []; + // } + + // return NodeMgr.getNodesByAsset(uuid); + // } + + // /** + // * 获取丢失资源的节点 + // * @returns uuids[] 节点数组 + // */ + // async queryNodesMissAsset() { + // const nodesUuid: string[] = []; + + // // 搜集 MissingScript + // const missScripts: { nodeUuid: string, scriptUuid: string }[] = []; + // EditorExtends.walkProperties( + // director.getScene()?.children, + // (obj: any, key: any, value: any, parsedObjects: any) => { + // // 搜集资源丢失的节点 + // if (value._uuid) { + // const isAssetMiss = !(cc.assetManager.assets.get(value._uuid) || cc.assetManager.assets.get(Editor.Utils.UUID.compressUUID(value._uuid, true))); + // if (isAssetMiss) { + // const node = findLast(parsedObjects, (item: any) => item instanceof cc.Node); + // if (node && !nodesUuid.includes(node.uuid)) { + // nodesUuid.push(node.uuid); + // } + // } + // } + // // 搜集 MissingScript(脚本丢失或者是编译不通过的脚本) + // if (value instanceof MissingScript) { + // // @ts-ignore __type__: 存储编译不通过或丢失的脚本 id + // const id = value._$erialized?.__type__; + // missScripts.push({ + // nodeUuid: value.node.uuid, + // scriptUuid: EditorExtends.UuidUtils.decompressUuid(id), + // }); + // } + // }, + // { + // dontSkipNull: false, + // ignoreSubPrefabHelper: true, + // }, + // ); + + // // 检测 MissingScript 的脚本是否真的丢失 + // const existingScriptResult = await Editor.Message.request('asset-db', 'batch-message-handler', missScripts.map((item: { nodeUuid: string, scriptUuid: string }) => { + // return { + // name: 'query-asset-info', + // args: [item.scriptUuid], + // }; + // })); + // const existingScriptUUIDs = existingScriptResult.map((info: IAssetInfo | null) => info && info.uuid); + // missScripts.forEach((item: { nodeUuid: string, scriptUuid: string }) => { + // if (!existingScriptUUIDs.includes(item.scriptUuid) && + // !nodesUuid.includes(item.nodeUuid)) { + // nodesUuid.push(item.nodeUuid); + // } + // }); + + // return nodesUuid; + // } + + /** + * 预览设置属性后的效果,不进入undo堆栈 + * @param uuid + * @param path + * @param dump + * @returns + */ + async previewSetNodeProperty(uuid: string, path: string, dump: IProperty): Promise { + const node = NodeMgr.getNode(uuid); + const info = dumpUtil.parsingPath(path, node); + if (!node) { + console.warn('previewSetNodeProperty failed:node not found', uuid); + return false; + } + if (!info.search) { + console.warn('previewSetNodeProperty failed:property path error', path); + return false; + } + // 需要自己记录设置前的属性,在取消时还原效果; + let target = get(node, info.search) ? get(node, info.search)[info.key] : undefined; + if (!target) { + // 属性为空时使用默认值 + target = dumpUtil.getDefaultValue(dump.type); + } + const data = dumpUtil.encodeObject(target, { + type: dump.type, + ctor: target.constructor, + }, target); + + // @ts-ignore + const cache: Map = this._previewPropertysCache.has(uuid) ? this._previewPropertysCache.get(uuid) : new Map(); + // 只有第一次预览时的数据,是节点原本的数据 + if (!cache?.has(path)) { + cache?.set(path, data); + } + this._previewPropertysCache.set(uuid, cache); + // 修改属性,false会避免记录undo操作; + return await this.setProperty(uuid, path, dump, false); + } + + async cancelPreviewSetNodeProperty(uuid: string, path: string): Promise { + // 拿到记录的数据,还原回数据 + const node = this.query(uuid); + const info = dumpUtil.parsingPath(path, node); + if (!node) { + console.warn('cancelPreviewSetNodeProperty failed:node not found', uuid); + return false; + } + if (!info.search) { + console.warn('cancelPreviewSetNodeProperty failed:property path error', path); + return false; + } + const cache = this._previewPropertysCache.get(uuid); + if (!cache) { + return false; + } + const dump = cache?.get(path); + if (!dump) { + return false; + } + // 清理掉原来的数据 + cache?.delete(path); + return await this.setProperty(uuid, path, dump, false); + } + + /** + * 设置一个节点的属性 + * @param {*} uuid + * @param {*} path + * @param {*} key + * @param {*} record 是否记录到undo堆栈上 + * @param {*} dump + */ + async setProperty(uuid: string, path: string, dump: IProperty, record = true): Promise { + // 多个节点更新值 + if (Array.isArray(uuid)) { + try { + for (let i = 0; i < uuid.length; i++) { + await this.setProperty(uuid[i], path, dump); + } + return true; + } catch (e) { + console.error(e); + return false; + } + } + const node = this.query(uuid); + if (!node) { + console.warn(`Set property failed: ${uuid} does not exist`); + return false; + } + + // 触发修改前的事件 + this.emit('node:before-change', node); + if (path === 'parent' && node.parent) { + // 发送节点修改消息 + this.emit('node:before-change', node.parent); + } + + // 恢复数据 + await dumpUtil.restoreProperty(node, path, dump); + + // 触发修改后的事件 + this.emit('node:change', node, { type: NodeOperationType.SET_PROPERTY, propPath: path, record: record }); + // 如果是数组的话,需要依次 emit change,路径定位到数组的下标位置 + if (dump.isArray && Array.isArray(dump.value)) { + dump.value.forEach((item, i) => { + this.emit('node:change', node, { type: NodeOperationType.SET_PROPERTY, propPath: `${path}.${i}`, record: record }); + }); + } + // 改变父子关系 + if (path === 'parent' && node.parent) { + // 发送节点修改消息 + this.emit('node:change', node.parent, { type: NodeOperationType.SET_PROPERTY, propPath: 'children', record: record }); + } + return true; + } + + /** + * 设置属性的默认值 + * @param {*} uuid + * @param {*} path + * @param {*} type + */ + async resetProperty(uuid: string, path: string): Promise { + // 多个节点更新值 + if (Array.isArray(uuid)) { + uuid.forEach((id) => { + this.resetProperty(id, path); + }); + return true; + } + const node = this.query(uuid); + if (!node) { + console.warn(`Set default value failed: ${uuid} does not exist`); + return false; + } + + // 触发修改前的事件 + this.emit('node:before-change', node); + + // 恢复数据 + await dumpUtil.resetProperty(node, path); + + // 触发修改后的事件 + this.emit('node:change', node, { type: NodeOperationType.SET_PROPERTY, propPath: path }); + return true; + } + + /** + * 将一个属性其现存值与定义类型值不匹配,或者为 null 默认值,改为一个可编辑的值 + * @param {*} uuid + * @param {*} path + */ + async updatePropertyFromNull(uuid: string, path: string): Promise { + // 多个节点更新值 + if (Array.isArray(uuid)) { + uuid.forEach((id) => { + this.updatePropertyFromNull(id, path); + }); + return true; + } + const node = this.query(uuid); + if (!node) { + console.warn(`Set default value failed: ${uuid} does not exist`); + return false; + } + + // 触发修改前的事件 + this.emit('node:before-change', node); + + // 恢复数据 + await dumpUtil.updatePropertyFromNull(node, path); + + // 触发修改后的事件 + this.emit('node:change', node, { type: NodeOperationType.SET_PROPERTY, propPath: path }); + return true; + } + + /** + * 重置节点属性 position rotation scale + * @param {*} uuid + */ + async resetNode(uuid: string): Promise { + // 多个节点更新值 + if (Array.isArray(uuid)) { + uuid.forEach((id) => { + this.resetNode(id); + }); + return true; + } + const node = this.query(uuid); + if (!node) { + console.warn(`Set default value failed: ${uuid} does not exist`); + return false; + } + + // 触发修改前的事件 + this.emit('node:before-change', node); + + // 恢复数据 + const properties = ['position', 'rotation', 'scale', 'mobility']; + for (const path of properties) { + await dumpUtil.resetProperty(node, path); + + // 触发修改后的事件 + this.emit('node:change', node, { type: NodeOperationType.SET_PROPERTY, propPath: path }); + } + return true; + } + + /** + * 设置某个节点连同它的子集的 layer 属性值 + * @param {*} uuid + * @param {*} dump + */ + async setNodeAndChildrenLayer(uuid: string, dump: any) { + await this.setProperty(uuid, 'layer', dump); + + const node = this.query(uuid); + + if (node && node.children && node.children.length > 0) { + node.children.forEach((child: any) => { + this.setNodeAndChildrenLayer(child.uuid, dump); + }); + } + } + + /** + * 调整一个数组类型的数据内某个 item 的位置 + * @param uuid 要被移动的节点或组件 + * @param path 数组的搜索路径 + * @param target 现在的索引位置 + * @param offset 偏移量 + */ + moveArrayElement(uuid: string, path: string, target: number, offset: number): boolean { + // TODO: deprecated 这一段 isArray 应该没有用到了,建议一段时间后可以删掉 + if (Array.isArray(uuid)) { + uuid.forEach((id) => { + this.moveArrayElement(id, path, target, offset); + }); + return false; + } + + const node = this.query(uuid); + if (!node) { + console.warn(`Move property failed: ${uuid} does not exist`); + return false; + } + + // 因为 path 内的 __comps__ 实际指向的是 _components + path = path.replace('__comps__', '_components'); + + // 找到指定的 data 数据 + const data = path ? get(node, path) : node; + if (!data) { + console.warn(`Move property failed: ${uuid} does not exist`); + return false; + } + + if (!Array.isArray(data)) { + console.warn(`Move property failed: ${uuid} - ${path} isn't an array`); + return false; + } + + // 发送节点修改消息 + this.emit('node:before-change', node); + + // 移动顺序 + if (path === 'children') { + // 过滤掉类似 Foreground Background 的节点 + const children = data.filter((child) => !(child.objFlags & cc.Object.Flags.HideInHierarchy)); + const child = children[target]; + + // 容错处理:新增的节点在引擎中还未创建,就指令其移动,setSiblingIndex 会报错 + if (!child) { + return false; + } + + // 找出要移动的节点在没有过滤掉隐藏节点的场景中的位置 + const index = data.indexOf(children[target + offset]); + + child.setSiblingIndex(index); + } else { + const temp = data.splice(target, 1); + data.splice(target + offset, 0, temp[0]); + + set(node, path, data); // 自身 = 自身(副本),为了兼顾材质需要整体赋值副本的情况 + } + + // 发送节点修改消息 + this.emit('node:change', node, { type: NodeOperationType.MOVE_ARRAY_ELEMENT, propPath: path }); + + return true; + } + + /** + * 删除一个数组元素 + * @param uuid 节点的 uuid + * @param path 元素所在数组的搜索路径 + * @param index 目标 item 原来的索引 + */ + removeArrayElement(uuid: string, path: string, index: number): boolean { + if (Array.isArray(uuid)) { + uuid.forEach((id) => { + this.removeArrayElement(id, path, index); + }); + return true; + } + const node = this.query(uuid); + const key = (path || '').split('.').pop(); + + if (key === 'children') { + console.warn('Unable to change `children` of the parent, Please change the `parent` of the child'); + return false; + } + + if (!node) { + console.warn(`Move property failed: ${uuid} does not exist`); + return false; + } + + // 因为 path 内的 __comps__ 实际指向的是 _components + path = path.replace('__comps__', '_components'); + + // 找到指定的 data 数据 + const data = path ? get(node, path) : node; + if (!data) { + console.warn(`Move property failed: ${uuid} does not exist`); + return false; + } + + if (!Array.isArray(data)) { + console.warn(`Move property failed: ${uuid} - ${path}.${key} isn't an array`); + return false; + } + + // 发送节点修改消息 + this.emit('node:before-change', node); + + // 删除components中的元素要通过调用removeComponent方法 + if (path === '_components') { + const comp = data[index]; + // https://github.com/cocos-creator/3d-tasks/issues/1116 + compMgr.removeComponent(comp); + } else { + // 删除某个 item + data.splice(index, 1); + + set(node, path, data); // 自身 = 自身(副本),为了兼顾材质需要整体赋值副本的情况 + } + + // 发送节点修改消息 + this.emit('node:change', node, { type: NodeOperationType.REMOVE_ARRAY_ELEMENT, propPath: path, index }); + + return true; + } + + /** + * 复制节点的动作,给下一步粘贴(创建)节点准备数据 + * @param {*} uuids 单个 string 或 array + */ + copyNode(uuids: string | string[]) { + if (!Array.isArray(uuids)) { + uuids = [uuids]; + } + + uuids = this.canRemoveOrCopy(uuids); + + stashInstants = {}; + + function changeFileId(node: Node) { + const prefabInfo = node['_prefab']; + + if (prefabInfo) { + if (prefabInfo.instance) { + return; + } else { + // 非prefabInstance节点,就变为普通节点来复制 + node['_prefab'] = null; + for (let i = 0; i < node.components.length; i++) { + const comp = node.components[i]; + comp.__prefab = null; + } + } + } + + if (node.children.length > 0) { + let index = node.children.length; + + // .children 是只读属性,需要用 splice + while (index--) { + const child = node.children[index]; + // 需要剔除不需要保存的私有节点 + const isPrivateNode = child.objFlags & cc.Object.Flags.HideInHierarchy; + const canDelete = child.objFlags & cc.Object.Flags.DontSave; + if (isPrivateNode && canDelete) { + node.removeChild(child); + // node.children.splice(index, 1); + } else { + changeFileId(child); + } + } + } + } + + for (const uuid of uuids) { + const node = this.query(uuid); + + if (!node) { + continue; + } + const instant = cc.instantiate(node); + + // Hack 目前 cc.instantiate 没有变动 fileId,这里变动一下,使它不重复 + changeFileId(instant); + + stashInstants[uuid] = { + instant, + }; + } + + return uuids; + } + + // /** + // * 复制节点自身 + // * 一般来自 ctrl + d 快捷键 + // * @param uuids + // */ + // duplicateNode(uuids: string | string[]) { + // if (!Array.isArray(uuids)) { + // uuids = [uuids]; + // } + + // const newUuids: string[] = []; + // const oldStashInstants = stashInstants; + // uuids = this.copyNode(uuids); + + // for (const uuid of uuids) { + // const node = this.query(uuid); + + // if (!node) { + // continue; + // } + + // const newUuid = this.createNode(node.parent?.uuid, null, uuid, false, true); + // if (newUuid) { + // newUuids.push(newUuid); + // } + // } + + // // 需要使用上次缓存的数据,否则粘贴会找不到上次拷贝的数据 #15805 + // stashInstants = oldStashInstants; + + // const results = newUuids.filter(Boolean); + + // // 选中新创建的节点 + // sceneSelection.clear(); + // results.forEach((uuid) => { + // sceneSelection.select(uuid); + // }); + + // return results; + // } + + // /** + // * 粘贴被复制的节点 + // * @param target + // * @param uuids + // * @param keepWorldTransform + // */ + // async pasteNode(target: string | null | undefined, uuids: string | string[], keepWorldTransform = false) { + // if (!Array.isArray(uuids)) { + // uuids = [uuids]; + // } + + // const newUuids: string[] = []; + + // for (const uuid of uuids) { + // const newUuid = this.createNode(target, null, uuid, keepWorldTransform, true); + // if (newUuid) { + // const node = this.query(newUuid); + // if (node) { + // // 预制体场景下,需要检查根节点有没有UI组件或canvas组件 + // const needCheckCanvasRequire = (cce.SceneFacadeManager.getCurrentFacade().modeName === SceneModeType.Prefab) && this.checkNodeUseCanvasRequired(node); + // const parent = await this.checkCanvasRequired(node, needCheckCanvasRequire, this.getNewNodeParent(target), undefined); + // parent !== node && node.setParent(parent, keepWorldTransform); + // } + // newUuids.push(newUuid); + // // 如果粘贴多个节点时,由于缺失了parent信息,会拿当前选中节点作为父节点 + // if (!target) { + // const node = this.query(newUuid); + // if (node) { + // target = node.parent?.uuid; + // } + // } + // } + // } + + // const results = newUuids.filter(Boolean); + + // // 选中新创建的节点 + // sceneSelection.clear(); + // results.forEach((uuid) => { + // sceneSelection.select(uuid); + // }); + + // return results; + // } + + /** + * 挂载节点,如拖入和剪切 + * @param parent + * @param uuids + * @param keepWorldTransform + */ + setParent(parent: string, uuids: string | string[], keepWorldTransform = false) { + if (!Array.isArray(uuids)) { + uuids = [uuids]; + } + + uuids = uuids.filter((uuid: string) => { + const node = this.query(uuid); + if (!node || !node.parent) { + return false; + } + return true; + }); + let parentNode: Node | null; + if (parent) { + parentNode = this.query(parent); + } + parentNode ||= director.getScene(); + + for (const uuid of uuids) { + const node = this.query(uuid); + node?.setParent(parentNode, keepWorldTransform); + } + + return uuids; + } + + /** + * 实时获取新节点在一个父节点下的有效名称 + * 规则是 Node 同名时为 Node-001 + * @param name 名称 + * @param parentUuid 父节点 uuid + */ + generateAvailableName(name: string, parentUuid?: string) { + if (!name) { + name = 'Node'; + } + + let parent = director.getScene() as Node; + + if (parentUuid) { + const node = this.query(parentUuid); + parent = node ? node : parent; + } + + return getNodeName(name, parent); + } + + // /** + // * 创建一个新节点 + // * @param uuid + // * @param name + // * @param stashUuid + // * @param keepWorldTransform + // * @param keepLayer + // */ + // createNode(uuid: string | null | undefined, name: any, stashUuid: string | null, keepWorldTransform = false, keepLayer = false): undefined | string { + // if (!cc.director.getScene()) { + // return; + // } + + // if (keepWorldTransform === null) { + // keepWorldTransform = true; + // } + + // /** + // * TODO: uuid 为 parentUuid + // * 理论上要在 prefab-scene-facade 的 createNode 中设置 + // * 注意 this.getNewNodeParent() 工具函数处理了无值情况下默认取第一个选中节点 + // * 如果提取到 prefab-scene-facade 设置,工具函数也要考虑 + // */ + // const parent = this.getNewNodeParent(uuid); + + // let node: Node | null = null; + + // if (stashUuid) { + // if (stashInstants[stashUuid]) { + // const { instant } = stashInstants[stashUuid]; + + // if (instant) { + // node = cc.instantiate(instant); + // if (node) { + // // 查找到第一层的PrefabInstance并设置新的FileId + // visitNode(node, (target) => { + // // @ts-ignore + // const prefabInfo = target['_prefab']; + // if (prefabInfo?.instance) { + // // 复制需要产生新的Prefab实例,因为需要不同的fileId + // prefabInfo.instance = prefabUtils.cloneInstanceWithNewFileId(prefabInfo.instance); + // return true; + // } + // }); + + // name = getNodeName(node.name, parent); + // } + // } + // } + // } + + // if (!node) { + // node = new cc.Node(); + // } + + // if (!node) { + // return; + // } + + // if (name) { + // node.name = name; + // } + + // /** + // * 新节点的 layer 跟随父级节点,但父级节点为场景根节点除外 + // * parent.layer 可能为 0 (界面下拉框为 None),此情况下新节点不跟随 + // */ + // if (parent.layer && parent !== director.getScene() && !keepLayer) { + // setLayer(node, parent.layer, true); + // } + + // this.emit('node:before-add', node); + // this.emit('node:before-change', parent); + + // node.setParent(parent, keepWorldTransform); + + // if (!stashUuid) { + // this.ensureUITransformComponent(node); + // } + + // // 发送添加节点事件,添加节点中的根节点 + // this.emit('node:add', node); + + // // 发送节点修改消息 + // if (parent) { + // this.emit('node:change', parent, { type: NodeEventType.CHILD_CHANGED }); + // } + + // // 选中新创建的节点 + // cce.Selection.clear(); + // cce.Selection.select(node.uuid); + + // return node.uuid; + // } + + /** + * 确保节点有 UITransform 组件 + * 目前只需保障在创建空节点的时候检查任意上级是否为 canvas + */ + ensureUITransformComponent(node: Node) { + if (node instanceof cc.Node && node.children.length === 0) { + // 空节点 + let inside = false; + let parent = node.parent; + + while (parent) { + const components = parent.components.map((comp) => cc.js.getClassName(comp.constructor)); + if (components.includes('cc.Canvas')) { + inside = true; + break; + } + parent = parent.parent; + } + + if (inside) { + try { + node.addComponent('cc.UITransform'); + } catch (error) { + console.error(error); + } + } + } + } + + async restorePrefab(uuid: string, assetUuid: string) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this; + + if (!director.getScene()) { + return false; + } + + // 先取消选中,暂存选中的节点 uuid + // const selectedUuids = cce.Selection.query(); + // cce.Selection.clear(); + + const query = this.query; + const prefabRoot = query(uuid) as Node; + + // 根据 fileId 缓存旧节点,以用于一些引用节点的还原 + const oldNodes: Record = {}; + /** + * 缓存 fileId 对应的旧节点 + * @param node 节点 + */ + function collectOldNodes(node: Node) { + if (!node || !node['_prefab']) { + return; + } + + oldNodes[node['_prefab'].fileId] = node; + + if (Array.isArray(node.children)) { + node.children.forEach((child) => { + collectOldNodes(child); + }); + } + } + + // 根据 fileId 缓存子集列表 uuids + const childrenUuid: Record> = {}; + function collectChildrenUuid(node?: Node) { + const rt: Record = {}; + + if (node) { + if (childrenUuid[node.uuid]) { + return childrenUuid[node.uuid]; + } + + if (Array.isArray(node.children)) { + node.children.forEach((child: any) => { + // @ts-ignore + if (child && child['_prefab']) { + // @ts-ignore + rt[child['_prefab'].fileId] = child.uuid; + } + }); + childrenUuid[node.uuid] = rt; + } + } + return rt; + } + + // 根据 fileId 缓存子集列表的正确索引 + const childrenIndex: Record> = {}; + function collectChildrenIndex(node: Node | undefined) { + const rt: Record = {}; + + if (node) { + if (childrenIndex[node.uuid]) { + return childrenIndex[node.uuid]; + } + + if (Array.isArray(node.children)) { + node.children.forEach((child, i) => { + // @ts-ignore + if (child && child['_prefab']) { + // @ts-ignore + rt[child['_prefab'].fileId] = i; + } + }); + childrenIndex[node.uuid] = rt; + } + } + return rt; + } + + /** + * 检查 dump 中的引用节点是否可用 + * 旧节点还存在的时候,新数据是不可用的,新数据里需要替换为旧节点的 uuid + * 旧节点不存在的时候,新数据便是可用的,因为新节点一定会替换上去。 + * @param dumpComps 组件 + */ + function redirectSceneRefs(dumpComps: any) { + dumpComps.forEach((comps: any) => { + if (!comps.value || typeof comps.value !== 'object') { + return; + } + + const keys = Object.keys(comps.value); + for (const key of keys) { + if (['node'].includes(key)) { + continue; + } + + const comp = comps.value[key]; + + // 递归到里层 + if (comp.isArray && Array.isArray(comp.value)) { + redirectSceneRefs(comp.value); + continue; + } + + if (comp.type === 'cc.Node') { + const newNode = query(comp.value.uuid); + + // 数据错误 + if (!newNode || !newNode?.['_prefab']?.fileId) { + continue; + } + const oldNode = oldNodes[newNode['_prefab'].fileId]; + if (oldNode) { + comp.value.uuid = oldNode.uuid; // 换为旧节点的 uuid + } + } + } + }); + } + + /** + * 还原现有节点的 dump ,删除多余节点,添加新节点 + * @param newNode 新节点 + * @param parentNode 新节点的父节点 + * @param prefabParent 新节点通过 fileId 指向现有节点的父节点 + */ + async function restore(newNode: Node, parentNode?: Node, prefabParent?: Node) { + // 私有节点不还原 + if (newNode.objFlags & cc.Object.Flags.HideInHierarchy) { + return false; + } + + const fileId2Index = collectChildrenIndex(parentNode); // 对应新数据上的子集排列 + const fileId2Uuid = collectChildrenUuid(prefabParent); // 对应新数据上的 uuid + + const dump = dumpUtil.dumpNode(newNode) as INode; + const fileId = dump.__prefab__!.fileId; + // 现有 prefab 节点 + const prefab = prefabParent ? query(fileId2Uuid[fileId]) : prefabRoot; + + if (prefab) { + // 如果现有的节点存在,只需还原 dump data + that.emit('node:before-change', prefab); + + // 删除掉不在新数据上的子节点 + if (Array.isArray(prefab.children)) { + const childrenFileId2Index = collectChildrenIndex(newNode); + + let index = 0; + let child = prefab.children[index]; + while (child && index < prefab.children.length) { + // @ts-ignore + if (child['_prefab'] && childrenFileId2Index[child['_prefab'].fileId] === undefined) { + that.removeNode(child.uuid); + } else { + index++; + } + child = prefab.children[index]; + } + } + + const prefabDump = dumpUtil.dumpNode(prefab) as INode; + + // 删除不必要的字段 + // Prefab 里的 dump 为什么需要删除 uuid + // @ts-ignore + delete dump.uuid; + // @ts-ignore + delete dump.children; + + // 不是根节点 + if (prefabParent) { + dump.parent.value.uuid = prefabParent.uuid; + } else { + // 如果是 prefab 根节点,有些属性不能还原 + dump.active.value = prefabDump.active.value; + dump.name.value = prefabDump.name.value; + dump.position.value = prefabDump.position.value; + dump.rotation.value = prefabDump.rotation.value; + } + + // 使用原来的数据 + dump.__prefab__ = JSON.parse(JSON.stringify(prefabDump.__prefab__)); + + // 检查一些属性上的值,其节点引用是否正确 + if (Array.isArray(dump.__comps__)) { + redirectSceneRefs(dump.__comps__); + } + + // prefab 为现有的 prefab 节点,用新数据 dump 还原内部属性和组件的值 + await dumpUtil.restoreNode(prefab, dump); + + // 确保位置准确 + if (fileId2Index[fileId] !== undefined) { + prefab.setSiblingIndex(fileId2Index[fileId]); + } + + // 逐层移动到目标节点上 + let index = 0; + let childNode = newNode.children[index]; + while (childNode && index < newNode.children.length) { + const isMoved = await restore(childNode, newNode, prefab); + if (!isMoved) { + index++; + } + childNode = newNode.children[index]; + } + + that.emit('node:change', prefab); + + // 没有移动 + return false; + } + // 现有节点不存在,则将临时的 prefab 中 fileId 一致的节点移动过来替换 + const newPrefab = newNode['_prefab']; + if (newPrefab && prefabParent) { + that.emit('node:before-add', newNode); + that.emit('node:before-change', prefabParent); + const fileID = newPrefab.fileId; + const index = fileId2Index[fileID]; + prefabParent.insertChild(newNode, index); + newPrefab.root = prefabParent['_prefab']?.root; + that.emit('node:add', newNode); + that.emit('node:change', prefabParent); + } + + // 有移动 + return true; + } + + try { + collectOldNodes(prefabRoot); + const asset = await loadAny(assetUuid); + const newNode = cc.instantiate(asset); + prefabRoot.parent?.addChild(newNode); + await restore(newNode); // 逐层还原 prefab + newNode.parent = null; // 删除临时节点 + + this.emit('node:change', prefabRoot); + } catch (error) { + console.warn('The prefab asset no longer exist.'); + console.error(error); + return false; + } + + // 重新选中,恢复 gizmos 状态 + // setTimeout(() => { + // selectedUuids.forEach((selectedUuid: string) => { + // cce.Selection.select(selectedUuid); + // }); + // }); + + return true; + } + // /** + // * 节点内包含UITransform组件,则需要父级也包含该组件, + // * canvas本身不需要Canvas组件 + // * @param node + // * @returns + // */ + // checkNodeUseCanvasRequired(node: Node | undefined): boolean { + // if (!node) return false; + // let flag = false; + // if (node.getComponent('cc.Canvas')) return false; + // node.walk((child) => { + // if (flag) { + // return; + // } + // child.components.forEach((component) => { + // if (flag) { + // return; + // } + // flag = js.getClassName(component) === 'cc.UITransform'; + // }); + // }); + + // return flag; + // } + + // /** + // * 检查并根据需要创建 canvas节点或为父级添加UITransform组件,返回父级节点,如果需要canvas节点,则父级节点会是canvas节点 + // * @param component + // * @param canvasRequiredParam + // * @param parent + // * @param position + // * @returns + // */ + // async checkCanvasRequired(node: Node | undefined, canvasRequiredParam: boolean | undefined, parent: Node, position: Vec3 | undefined): Promise { + + // /** + // * TODO: mode 判断不应该在底层这里 + // * 从 assets 拖图片到场景,不会走 prefab-scene-facade 的 createNode + // * 直接从 onDrop 走到了这里,先保持这样,后续改 + // */ + // const mode = cce.SceneFacadeManager.queryMode(); + // let prefabProxy: PrefabSceneProxy | null = null; + // let prefabRoot = null; + + // if (mode === 'prefab') { + // prefabProxy = cce.SceneFacadeManager['_facadeFSM'].prefabSceneFacade['_sceneProxy'] as PrefabSceneProxy; + // prefabRoot = prefabProxy.getRootNode(); + + // // prefab 的场景节点是临时的根节点,需转为 prefab root node + // if (parent === director.getScene()) { + // parent = prefabRoot; + // } + // } + + // if (canvasRequiredParam) { + // let canvasNode: Node | null = null; + + // // 在 prefab 模式下,询问是否给根节点添加 UITransform 组件还是父级添加一个 Canvas 节点 + // if (mode === 'prefab') { + // canvasNode = getUICanvasNode(parent, false); + + // const UITransformParentNode = getUITransformParentNode(parent); + // if (!canvasNode) { + // if (!UITransformParentNode) { + // const result = await Editor.Dialog.warn(Editor.I18n.t('scene.contributions.messages.description.UITransform_lack'), { + // buttons: [ + // Editor.I18n.t('scene.contributions.messages.description.UITransform_add_to_root'), + // Editor.I18n.t('scene.contributions.messages.description.UITransform_within_canvas'), + // Editor.I18n.t('scene.contributions.messages.description.UITransform_cancel'), + // ], + // default: 0, + // cancel: 2, + // }); + + // if (result.response === 0) { + // if (!prefabRoot) { + // console.error('prefab Root is not exist'); + // return null; + // } + // prefabRoot.addComponent('cc.UITransform'); + + // if (prefabRoot.parent && !hasOneKindOfComponent(prefabRoot.parent, Canvas)) { + // // 为了显示,节点结构为:scene node > canvas node > prefab root node + // canvasNode = await createShouldHideInHierarchyCanvasNode(director.getScene()!); + // prefabRoot.parent = canvasNode; + // } + // } + // if (result.response === 2) { + // canvasNode = new Node(); + // } + // } else { + // canvasNode = UITransformParentNode; + // } + // } else { + // if (canvasNode.parent !== director.getScene()) { + // parent = canvasNode; + // } + // } + // } else { + // canvasNode = getUICanvasNode(parent); + // if (canvasNode) { + // parent = canvasNode; + // } + // } + + // // 自动创建一个 canvas 节点 + // if (!canvasNode) { + // let canvasAssetUuid = 'f773db21-62b8-4540-956a-29bacf5ddbf5'; + + // // 2d 项目创建的 ui 节点,canvas 下的 camera 的 visibility 默认勾上 default + // if (cce.SceneFacadeManager['_projectType'] === '2d') { + // canvasAssetUuid = '4c33600e-9ca9-483b-b734-946008261697'; + // } + + // assetManager.assets.remove(canvasAssetUuid); + // const canvasAsset = await loadAny(canvasAssetUuid); + // canvasNode = cc.instantiate(canvasAsset) as Node; + // cce.Prefab.removePrefabInfoFromNode(canvasNode); + + // parent.addChild(canvasNode); + // parent = canvasNode; + // } + + // // 目前 canvas 默认 z 为 1,而拖放到 Canvas 的控件因为检测的是 z 为 0 的平面,所以这边先强制把 z 设置为和 canvas 的一样 + // if (position) { + // position.z = canvasNode.position.z; + // } + // } + // return parent; + // } + + // /** + // * 从一个资源创建对应的节点 + // * @param {*} parentUuid + // * @param {*} assetUuid + // * @param {*} options { type: 资源类型, position: 位置坐标(vector3), name: 新的名字, canvasRequired?: true 是否需要有 cc.Canvas 父级 } + // */ + // async createNodeFromAsset(parentUuid: string | undefined | null, assetUuid: string, options: CreateNodeOptions): Promise { + // try { + // if (!cc.director.getScene()) return; + + // let parent: Node | null = this.getNewNodeParent(parentUuid); + // const { name, type, position, unlinkPrefab } = options; + + // // 有 assetUuid 时 type 默认可以为空 + // if (type && !creatableAssetTypes.includes(type)) return; + + // if (DEBUG_TIME_COST) { + // console.time('createNodeFromAsset'); + // } + + // if (options.autoAdaptToCreate === undefined) { + // options.autoAdaptToCreate = true; + // } + + // const { node, canvasRequired } = await createNodeByAsset({ + // uuid: assetUuid, + // type: type, + // canvasRequired: options.canvasRequired, + // autoAdaptToCreate: options.autoAdaptToCreate, + // }); + + // parent = await this.checkCanvasRequired(node, Boolean(canvasRequired), parent, position as Vec3) as Node; + + // if (name) { + // // 使用创建时指定的名称 + // node.name = basename(name, extname(name)); + // } + + // if (DEBUG_TIME_COST) { + // console.time('addNodeEvent'); + // } + + // this.emit('node:before-add', node); + // this.emit('node:before-change', parent); + + // /** + // * 默认创建节点是从 prefab 模板,所以初始是 prefab 节点 + // * 是否要 unlink 为普通节点 + // */ + // if (type !== 'cc.Prefab' || unlinkPrefab) { + // cce.Prefab.removePrefabInfoFromNode(node); + + // /** + // * 新节点的 layer 跟随父级节点,但父级节点为场景根节点除外 + // * parent.layer 可能为 0 (界面下拉框为 None),此情况下新节点不跟随 + // */ + // if (parent.layer && parent !== director.getScene()) { + // setLayer(node, parent.layer, true); + // } + // } + + // parent.addChild(node); + + // if (position) { + // node.setWorldPosition(position); + // } + + // // 发送添加节点事件,添加节点中的根节点. 'added'事件会在添加的所有节点(包括子节点)上发送 + // this.emit('node:add', node); + // // 发送节点修改消息 + // this.emit('node:change', parent, { type: NodeEventType.CHILD_CHANGED }); + + // if (DEBUG_TIME_COST) { + // console.timeEnd('addNodeEvent'); + // } + // // 选中新创建的节点 + // cce.Selection.clear(); + // cce.Selection.select(node.uuid); + + // if (DEBUG_TIME_COST) { + // console.timeEnd('createNodeFromAsset'); + // } + // return node.uuid; + // } catch (error) { + // console.error(error); + // return; + // } + // } + + private _walkNode(node: Node, func: Function) { + node && node.children && node.children.forEach((child) => { + func(child); + this._walkNode(child, func); + }); + } + + public baseRemoveNode(node: Node, keepWorldTransform?: boolean) { + // 增加容错 + if (!node) { + return; + } + + const parent = node.parent; + + // 发送节点修改消息 + this.emit('node:before-remove', node); + if (parent) { + this.emit('node:before-change', parent); + } + + //console.time('NodeMgr::removeNode'); + node.setParent(null, keepWorldTransform); + node._objFlags |= CCObject.Flags.Destroyed; + // 3.6.1 特殊 hack,请在后续版本移除 + // 相关修复 pr: https://github.com/cocos/cocos-editor/pull/890 + try { + this._walkNode(node, (child: any) => { + child._objFlags |= CCObject.Flags.Destroyed; + }); + } catch (error) { + console.warn(error); + } + + //console.timeEnd('NodeMgr::removeNode'); + + // 被删除节点里的根节点 + this.emit('node:remove', node, { source: EventSourceType.EDITOR }); + } + + /** + * 删除节点 + * @param {*} uuids + * @param {*} keepWorldTransform + */ + removeNode(uuids: string | string[], keepWorldTransform?: boolean) { + if (!Array.isArray(uuids)) { + uuids = [uuids]; + } + + uuids = this.canRemoveOrCopy(uuids); + + for (const uuid of uuids) { + const node: Node | null = this.query(uuid); + if (!node) { + continue; + } + this.baseRemoveNode(node, keepWorldTransform); + } + } + + /** + * 锁定一个节点不让其在场景中被选中 + * @param uuids 节点uuid + * @param locked true | false + * @param loop true | false 是否循环子孙级节点设置 + */ + changeNodeLock(uuids: string | string[], locked: boolean, loop: boolean) { + if (!Array.isArray(uuids)) { + uuids = [uuids]; + } + + for (const uuid of uuids) { + const node = this.query(uuid); + + // 增加容错 + if (!node) { + continue; + } + + this.emit('node:before-change', node); + + try { + if (locked) { + node.objFlags |= cc.Object.Flags.LockedInEditor; + } else { + node.objFlags &= ~cc.Object.Flags.LockedInEditor; + } + } catch (error) { + console.error(error); + } + + this.emit('node:change', node); + + // 处理内循环的情况 + if (loop === true && node.children && node.children.length > 0) { + node.children.forEach((child: any) => { + this.changeNodeLock(child.uuid, locked, loop); + }); + } + } + } + + /** + * 过滤根节点 + * 过滤子父包含的关系,只留下彼此独立的父节点 uuid + * @param uuids + */ + canRemoveOrCopy(uuids: string[]) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const t: any = this; + + const rt: string[] = []; + + // 剔除根节点或其他不可删除的节点 + const nodeUuids: string[] = []; + for (const uuid of uuids) { + const node = t.query(uuid); + + if (!node || !node.parent || node.objFlags & cc.Object.Flags.DontDestroy) { + continue; + } + + nodeUuids.push(uuid); + } + + // 剔除已在列表中其他节点的子节点 + for (const uuid of nodeUuids) { + const node = t.query(uuid); + if (!isChild(node)) { + rt.push(uuid); + } + } + + /** + * 是否是已在的列表中其他节点的子节点 + * @param node + */ + function isChild(node: any): boolean { + if (!node.parent) { + return false; + } + + if (nodeUuids.includes(node.parent.uuid)) { + return true; + } else { + return isChild(node.parent); + } + } + + return rt; + } + + // /** + // * 获取创建节点时所在的父节点 + // * @param uuid 父节点 + // */ + // getNewNodeParent(uuid: string | null | undefined): Node { + // let parent; + + // if (uuid) { + // parent = this.query(uuid); + // } else { + // /** + // * 如果有选中的节点,默认挂在第一个选中节点里 + // * 如果没有,挂在场景根节点里 + // */ + // const selects = cce.Selection.query(); + // if (Array.isArray(selects) && selects[0]) { + // parent = this.query(selects[0]); + // } else { + // parent = director.getScene(); + // } + // } + + // if (!parent) { + // parent = director.getScene(); + // } + + // // 不应该是Node里的逻辑 + // const mode = cce.SceneFacadeManager.queryMode(); + // if (mode === 'prefab') { + // const prefabProxy = cce.SceneFacadeManager['_facadeFSM'].prefabSceneFacade['_sceneProxy']; + // const prefabRoot = prefabProxy.getRootNode(); + + // // prefab 的场景节点是临时的根节点,需转为 prefab root node + // if (parent === cc.director.getScene()) { + // parent = prefabRoot; + // } + // } + // return parent as Node; + // } + + // changeNodeUUID(oldUUID: string | undefined, newUUID: string | undefined) { + // if (!oldUUID || !newUUID) { + // return; + // } + + // NodeMgr.changeNodeUUID(oldUUID, newUUID); + // } + + addComponentAt(node: Node, comp: Component, index: number): boolean { + if (!node || !comp || index < 0) { + return false; + } + + if (comp instanceof MissingScript && !comp._$erialized) { + return false; + } + + // @ts-ignore + node._addComponentAt(comp, index); + compMgr.emit('component:add', comp); + + return true; + } +} + +export default new NodeManager(); diff --git a/src/core/scene/scene-process/service/node/node-create.ts b/src/core/scene/scene-process/service/node/node-create.ts index 2bcabbf9c..36f3ccfe7 100644 --- a/src/core/scene/scene-process/service/node/node-create.ts +++ b/src/core/scene/scene-process/service/node/node-create.ts @@ -1,39 +1,12 @@ import type { - Mesh, - VideoClip, - BitmapFont, - TTFFont, - LabelAtlas, - ParticleAsset, - AnimationClip, - AudioClip, - TerrainAsset, - TiledMapAsset, Asset, Prefab, - SpriteFrame, } from 'cc'; import { - js, assetManager, Node, - Layers, Canvas, - UITransform, - Animation, - AudioSource, - Label, - MeshRenderer, - Sprite, - VideoPlayer, - ParticleSystem2D, - director, - SpriteRenderer, - Terrain, - TiledMap, - dragonBones, - sp, Scene, instantiate, CCObject, diff --git a/src/core/scene/scene-process/service/node/node-utils.ts b/src/core/scene/scene-process/service/node/node-utils.ts index 1d152476d..9a740f6ff 100644 --- a/src/core/scene/scene-process/service/node/node-utils.ts +++ b/src/core/scene/scene-process/service/node/node-utils.ts @@ -94,6 +94,33 @@ export function hasOneKindOfComponent(node: Node | Scene, kind: any) { return false; } +/** + * 生成一个 node-001 格式的可用节点名称 + * @param name 被检查的名称 + * @param parent 父级节点 + * @returns {string} path 可用名称的文件路径 + */ +export function getNodeName(name: string, parent: Node) { + if (!parent || !Array.isArray(parent.children)) { + return name; + } + + const names = parent.children.map((child: Node) => (child && child.name) || ''); + + while (names.includes(name)) { + if (/(\d+)$/.test(name)) { + name = name.replace(/(\d+)$/, (strA: string, strB: string) => { + let num = parseInt(strB, 10); + num += 1; + return num.toString().padStart(strB.length, '0'); + }); + } else { + name += '-001'; + } + } + + return name; +} /** * 设置节点层级 diff --git a/src/core/scene/scene-process/service/prefab.ts b/src/core/scene/scene-process/service/prefab.ts index f04b1e20f..fc23cca5d 100644 --- a/src/core/scene/scene-process/service/prefab.ts +++ b/src/core/scene/scene-process/service/prefab.ts @@ -10,8 +10,8 @@ import type { IGetPrefabInfoParams, IIsPrefabInstanceParams, INode, + IPrefab, IPrefabEvents, - IPrefabInfo, IPrefabService, IRevertToPrefabParams, IUnpackPrefabInstanceParams, @@ -51,7 +51,7 @@ export class PrefabService extends BaseService implements IPrefab if (!node) { throw new Error('创建预制体资源失败,返回结果为 null'); } - return sceneUtils.generateNodeInfo(node, false); + return await sceneUtils.generateNodeDump(node) as INode; } catch (e) { console.error(`创建预制体失败: 节点路径: ${params.nodePath} 资源 URL: ${params.dbURL} 错误信息:`, e); throw e; @@ -106,7 +106,7 @@ export class PrefabService extends BaseService implements IPrefab } this.unWrapPrefabInstance(node.uuid, !!params.recursive); - return sceneUtils.generateNodeInfo(node, true); + return await sceneUtils.generateNodeDump(node) as INode; } catch (e) { console.error(`解耦为普通节点失败:节点路径 ${params.nodePath} 是否递归: ${params.recursive} 错误信息:`, e); throw e; @@ -129,14 +129,14 @@ export class PrefabService extends BaseService implements IPrefab /** * 获取节点的预制体信息 */ - async getPrefabInfo(params: IGetPrefabInfoParams): Promise { + async getPrefabInfo(params: IGetPrefabInfoParams): Promise { try { const node = EditorExtends.Node.getNodeByPathOrThrow(params.nodePath); const prefabInfo = prefabUtils.getPrefab(node); if (!prefabInfo) { return null; } - return sceneUtils.generatePrefabInfo(prefabInfo) as IPrefabInfo; + return sceneUtils.generatePrefabDump(node); } catch (e) { console.error(`获取节点的预制体信息失败:节点路径 ${params.nodePath} 错误信息:`, e); throw e; diff --git a/src/core/scene/scene-process/service/prefab/component.ts b/src/core/scene/scene-process/service/prefab/component.ts index cea2bb448..c6cc93d37 100644 --- a/src/core/scene/scene-process/service/prefab/component.ts +++ b/src/core/scene/scene-process/service/prefab/component.ts @@ -326,7 +326,7 @@ class ComponentOperation { } public async cloneComponentToNode(node: Node, clonedComp: Component) { - const copyCompDump = dumpUtil.dumpComponentForEditor(clonedComp); + const copyCompDump = dumpUtil.dumpComponent(clonedComp); // 不要同步_objFlags,否则因为没有onEnable的标记会导致onDisable不被调用 // delete copyCompDump.value._objFlags; const newComp = node.addComponent(js.getClassName(clonedComp)); diff --git a/src/core/scene/scene-process/service/scene/utils.ts b/src/core/scene/scene-process/service/scene/utils.ts index c5b57861e..b17c0462b 100644 --- a/src/core/scene/scene-process/service/scene/utils.ts +++ b/src/core/scene/scene-process/service/scene/utils.ts @@ -1,21 +1,10 @@ -import cc, { Component } from 'cc'; -import { - IComponent, - IComponentIdentifier, - IMountedChildrenInfo, - IMountedComponentsInfo, - INode, - IPrefab, - IPrefabInfo, - IPrefabInstance, - IPropertyOverrideInfo, - ITargetInfo, - ITargetOverrideInfo, - OptimizationPolicy, -} from '../../../common'; +import cc, { Scene } from 'cc'; import compMgr from '../component/index'; import { prefabUtils } from '../prefab/utils'; -import dumpUtil from '../dump'; +import dumpUtil, { translateDumpI18n } from '../dump'; +import { encodePrefab } from '../dump/encode'; +import type { INode, IPrefab } from '../../../common'; +import type { IScene } from '../../../common/editor/scene'; class SceneUtil { /** 默认超时:1分钟 */ @@ -65,109 +54,65 @@ class SceneUtil { /** * 生成组件信息 */ - generateComponentInfo(component: cc.Component): IComponentIdentifier { - return compMgr.getComponentIdentifier(component); + generateComponentInfo(component: cc.Component) { + return this.generateComponentIdentifier(component); } - generatePrefabInfo(prefab: cc.Prefab._utils.PrefabInfo | null): IPrefabInfo | null { - if (!prefab) { - return null; + // hack: 在 IPrefab 上附加 CLI 所需的丰富结构(INodeIdentifier), + // proxy 层的 convertPrefab 读取这些字段构建 IPrefabInfo + // __asset__ 和 __nested_roots__ 无法从 IPrefab 字段推导,仍需 hack + // 同时为 IPrefab 中所有 node/component 引用填充 path/name + enrichPrefabDump(prefab: any, enginePrefab: any): void { + if (!prefab || !enginePrefab) return; + + prefab.__asset__ = enginePrefab.asset ? { + name: enginePrefab.asset.name, + uuid: enginePrefab.asset._uuid, + data: this.generateNodeIdentifier(enginePrefab.asset.data), + optimizationPolicy: enginePrefab.asset.optimizationPolicy, + persistent: enginePrefab.asset.persistent, + } : undefined; + prefab.__nested_roots__ = enginePrefab.nestedPrefabInstanceRoots + ? enginePrefab.nestedPrefabInstanceRoots.map((node: cc.Node) => this.generateNodeIdentifier(node)) + : []; + + // 为 root 填充 path/name + if (enginePrefab.root) { + prefab.__root__ = this.generateNodeIdentifier(enginePrefab.root); } - const generateTargetInfo = (info: any): ITargetInfo | null => { - if (!info) { - return null; - } - return { - localID: info.localID, - }; - }; - - const generatePropertyOverrideInfo = (info: any): IPropertyOverrideInfo => { - return { - targetInfo: generateTargetInfo(info.targetInfo), - propertyPath: info.propertyPath, - value: info.value, - }; - }; - - const generateMountedChildrenInfo = (info: any): IMountedChildrenInfo => { - return { - targetInfo: generateTargetInfo(info.targetInfo), - nodes: info.nodes.map((node: cc.Node) => this.generateNodeIdentifier(node)) - }; - }; - - const generateMountedComponentsInfo = (info: any): IMountedComponentsInfo => { - return { - targetInfo: generateTargetInfo(info.targetInfo), - components: info.components.map((comp: cc.Component) => this.generateComponentIdentifier(comp)), - }; - }; - - const generatePrefabInstance = (instance: any): IPrefabInstance | undefined => { - if (!instance) { - return undefined; - } - const result = { - fileId: instance.fileId, - prefabRootNode: instance.prefabRootNode ? this.generateNodeIdentifier(instance.prefabRootNode) : undefined, - mountedChildren: instance.mountedChildren.map(generateMountedChildrenInfo), - mountedComponents: instance.mountedComponents.map(generateMountedComponentsInfo), - propertyOverrides: instance.propertyOverrides.map(generatePropertyOverrideInfo), - removedComponents: instance.removedComponents.map(generateTargetInfo), - }; - //prefabRootNode is optional field. - if (!result.prefabRootNode) { - delete result.prefabRootNode; - } - return result; - }; - - const generatePrefabAsset = (asset: any): IPrefab | undefined => { - if (!asset) { - return undefined; + // 为 targetOverrides 中的 source/target 填充 path/name(通过 UUID 查找) + if (prefab.targetOverrides) { + for (const override of prefab.targetOverrides) { + if (override.source) { + const node = EditorExtends.Node.getNode(override.source); + if (node) { + override.__source__ = this.generateNodeIdentifier(node); + } + } + if (override.target) { + const node = EditorExtends.Node.getNode(override.target); + if (node) { + override.__target__ = this.generateNodeIdentifier(node); + } + } } - return { - name: asset.name, - uuid: asset._uuid, - data: this.generateNodeIdentifier(asset.data), - optimizationPolicy: asset.optimizationPolicy as OptimizationPolicy, - persistent: asset.persistent, - }; - }; + } - const generateTargetOverrideInfo = (info: any): ITargetOverrideInfo => { - return { - source: info.source ? (info.source.node ? this.generateNodeIdentifier(info.source.node) : this.generateComponentIdentifier(info.source)) : null, - sourceInfo: generateTargetInfo(info.sourceInfo), - propertyPath: info.propertyPath, - target: info.target ? this.generateNodeIdentifier(info.target) : null, - targetInfo: generateTargetInfo(info.targetInfo), + // 为 instance 添加 hack 字段,仅包含需要 path/name 富化的标识符 + if (enginePrefab.instance) { + const inst = enginePrefab.instance; + const d = (prefab as any); + d.__instance__ = { + prefabRootNode: inst.prefabRootNode ? this.generateNodeIdentifier(inst.prefabRootNode) : undefined, + mountedChildren: (inst.mountedChildren ?? []).map((mc: any) => ({ + nodes: (mc.nodes ?? []).map((n: cc.Node) => this.generateNodeIdentifier(n)), + })), + mountedComponents: (inst.mountedComponents ?? []).map((mc: any) => ({ + components: (mc.components ?? []).map((c: cc.Component) => this.generateComponentIdentifier(c)), + })), }; - }; - - const root = prefab.root && this.generateNodeIdentifier(prefab.root); - - const result = { - asset: generatePrefabAsset(prefab.asset) ?? undefined, - root: root ?? undefined, - instance: generatePrefabInstance(prefab.instance) ?? undefined, - fileId: prefab.fileId, - targetOverrides: prefab.targetOverrides ? prefab.targetOverrides.map(generateTargetOverrideInfo) : [], - nestedPrefabInstanceRoots: prefab.nestedPrefabInstanceRoots ? prefab.nestedPrefabInstanceRoots.map((node: cc.Node) => this.generateNodeIdentifier(node)) : [], - }; - // asset, root, instance is a optional field in SchemaPrefabInfo. - if (!result.asset) { - delete result.asset; - } - if (!result.root) { - delete result.root; } - if (!result.instance) { - delete result.instance; - } - return result; } generateNodeIdentifier(node: cc.Node) { @@ -179,54 +124,76 @@ class SceneUtil { } generateComponentIdentifier(component: cc.Component) { - return compMgr.getComponentIdentifier(component); + return { + cid: (component as any).__cid__, + path: compMgr.getPathFromUuid(component.uuid) ?? '', + uuid: component.uuid, + name: component.name, + type: cc.js.getClassName(component.constructor), + enabled: component.enabled ? true : false, + }; } - /** - * 节点 dump 数据 - * @param node - * @param generateChildren - */ - generateNodeInfo(node: cc.Node, generateChildren: boolean, generateComponent = false): INode { - const identifier = this.generateNodeIdentifier(node); - const nodeInfo: INode = { - ...identifier, - prefab: this.generatePrefabInfo(node['_prefab']), - properties: { - active: node.active, - position: node.position, - rotation: node.rotation, - scale: node.scale, - layer: node.layer, - eulerAngles: node.eulerAngles, - mobility: node.mobility, - }, - components: [], - }; - if (node.components) { - nodeInfo.components = node.components - .map((component: cc.Component) => { - if (generateComponent) { - const path = compMgr.getPathFromUuid(component.uuid); - if (!path) throw Error('can not find component:`${component.uuid}`'); - const comp = compMgr.queryFromPath(path); - if (!comp) throw Error('can not find component path: `${path}`'); - return dumpUtil.dumpComponent(comp as Component) as IComponent; - } else { - const obj = this.generateComponentInfo(component) as IComponent; - return obj; - } - }); + generatePrefabDump(node: cc.Node): IPrefab | null { + const prefab = encodePrefab(node as any); + if (!prefab) return null; + this.enrichPrefabDump(prefab, node['_prefab']); + return prefab; + } + + async generateNodeDump(node: cc.Node): Promise { + if (node instanceof Scene) { + const sceneDump = await translateDumpI18n(dumpUtil.dumpNode(node)) as IScene; + + // hack: 以下字段不属于编辑器 dump 结构(IScene),仅用于 proxy 层将复杂的 dump 转换为 CLI 所需的扁平结构 + const d = sceneDump as any; + d.__path__ = '/'; + d.__position__ = { x: node.position.x, y: node.position.y, z: node.position.z }; + d.__rotation__ = { x: node.rotation.x, y: node.rotation.y, z: node.rotation.z, w: node.rotation.w }; + d.__scale__ = { x: node.scale.x, y: node.scale.y, z: node.scale.z }; + d.__layer__ = node.layer; + d.__mobility__ = node.mobility; + d.__prefab__ = encodePrefab(node as any); + if (d.__prefab__) { + this.enrichPrefabDump(d.__prefab__, node['_prefab']); + } + d.__comps__ = []; + for (const comp of node.components) { + const compDump = await translateDumpI18n(dumpUtil.dumpComponent(comp as cc.Component)) as any; + compDump.__component_path__ = compMgr.getPathFromUuid(comp.uuid) ?? ''; + compDump.__compPrefab__ = (comp as any).__prefab || null; + d.__comps__.push(compDump); + } + d.__childNodes__ = []; + for (const child of node.children) { + d.__childNodes__.push(await this.generateNodeDump(child) as INode); + } + return sceneDump; } - if (generateChildren) { - node.children.forEach((child) => { - if (!nodeInfo.children) { - nodeInfo.children = []; - } - nodeInfo.children.push(this.generateNodeInfo(child, true, false)); - }); + + const dump = await translateDumpI18n(dumpUtil.dumpNode(node)) as INode; + + // hack: 以下字段不属于编辑器 dump 结构(INode),仅用于 proxy 层将复杂的 dump 转换为 CLI 所需的扁平结构 + const d = dump as any; + d.__path__ = EditorExtends.Node.getNodePath(node); + d.__rotation__ = { x: node.rotation.x, y: node.rotation.y, z: node.rotation.z, w: node.rotation.w }; + if (dump.__prefab__) { + this.enrichPrefabDump(dump.__prefab__, node['_prefab']); + } + if (dump.__comps__) { + for (let i = 0; i < dump.__comps__.length && i < node.components.length; i++) { + const comp = node.components[i]; + (dump.__comps__[i] as any).__component_path__ = compMgr.getPathFromUuid(comp.uuid) ?? ''; + (dump.__comps__[i] as any).__compPrefab__ = (comp as any).__prefab || null; + } } - return nodeInfo; + + d.__childNodes__ = []; + for (const child of node.children) { + d.__childNodes__.push(await this.generateNodeDump(child)); + } + + return dump; } /** diff --git a/src/core/scene/scene-process/service/service-manager.ts b/src/core/scene/scene-process/service/service-manager.ts index 34e64544e..c870d6a6f 100644 --- a/src/core/scene/scene-process/service/service-manager.ts +++ b/src/core/scene/scene-process/service/service-manager.ts @@ -37,9 +37,9 @@ const SERVICE_EVENTS_MAP: EventMap = { 'component:remove': 'onRemoveComponent', 'component:added': 'onComponentAdded', 'component:removed': 'onComponentRemoved', - 'component:before-remove': 'onBeforeRemoveComponent', 'component:set-property': 'onSetPropertyComponent', - 'component:before-add-component': 'onBeforeComponentAdded', + 'component:before-add-component': 'onBeforeAddComponent', + 'component:before-remove-component': 'onBeforeRemoveComponent', // Script 事件 'script:execution-finished': 'onScriptExecutionFinished', diff --git a/src/core/scene/test/component-for-editor.testcase.ts b/src/core/scene/test/component-for-editor.testcase.ts new file mode 100644 index 000000000..6a111bda1 --- /dev/null +++ b/src/core/scene/test/component-for-editor.testcase.ts @@ -0,0 +1,517 @@ +/* + * TODO(qgh):理论上不应该放在这里测试,因为这些接口不是通过proxy调用的 + 而是直接通过service调用的。因为目前还未实现service接口的直接测试,因此是简单的实现。 + 后续需要迁移 + */ +import { + type ICreateByNodeTypeParams, + type IDeleteNodeParams, + type IAddComponentOptions, + type IQueryComponentOptions, + type ISetPropertyOptions, + type IComponentInfo, + type IComponent, + type IQueryClassesOptions, + type IExecuteComponentMethodOptions, + NodeType, +} from '../common'; +import { type ISetPropertyOptionsInfo } from '../common/cli/component'; +import { ComponentProxy } from '../main-process/proxy/component-proxy'; +import { NodeProxy } from '../main-process/proxy/node-proxy'; +import { EditorProxy } from '../main-process/proxy/editor-proxy'; +import { Rpc } from '../main-process/rpc'; +import { SceneTestEnv } from './scene-test-env'; + +// 这些接口未在 IPublicComponentService 中暴露,测试中直接通过 RPC 调用 +const rpcRequest = (method: string, args?: any[]) => + (Rpc.getInstance() as any).request('Component', method, args); + +function createComponent(params: IAddComponentOptions): Promise { + return rpcRequest('add', [params]); +} + +function resetComponent(params: IQueryComponentOptions): Promise { + return rpcRequest('reset', [params]); +} + +function queryClasses(options?: IQueryClassesOptions): Promise<{ name: string }[]> { + return rpcRequest('queryClasses', [options]); +} + +function queryComponentFunctionOfNode(path: string): Promise { + return rpcRequest('queryFunctionOfNode', [path]); +} + +function executeComponentMethod(options: IExecuteComponentMethodOptions): Promise { + return rpcRequest('executeMethod', [options]); +} + +function queryComponentHasScript(name: string): Promise { + return rpcRequest('hasScript', [name]); +} + +function queryComponentDump(path: string): Promise { + return rpcRequest('query', [path]); +} + +function setComponentPropertyForEditor(options: ISetPropertyOptions): Promise { + return rpcRequest('setProperty', [options]); +} + +const rpcNodeRequest = (method: string, args?: any[]) => + (Rpc.getInstance() as any).request('Node', method, args); + +describe('Component ForEditor 接口测试', () => { + let nodePath = ''; + + beforeAll(async () => { + await EditorProxy.open({ + urlOrUUID: SceneTestEnv.sceneURL, + }); + const params: ICreateByNodeTypeParams = { + path: 'TestNode', + nodeType: NodeType.EMPTY, + position: { x: 1, y: 2, z: 0 }, + }; + const testNode = await NodeProxy.createByType(params); + expect(testNode).toBeDefined(); + if (!testNode) { + return; + } + nodePath = testNode.path; + }); + + afterAll(async () => { + try { + const params: IDeleteNodeParams = { + path: nodePath, + keepWorldTransform: false, + }; + await NodeProxy.delete(params); + } catch (e) { + console.log(`删除节点失败 ${e}`); + throw e; + } + await EditorProxy.close({}); + }); + + describe('1. createComponent - 创建组件测试', () => { + it('create - 创建已知组件应返回组件信息', async () => { + const options: IAddComponentOptions = { + nodePath: nodePath, + component: 'cc.Label', + }; + try { + const result = await createComponent(options); + expect(result).toBeDefined(); + // 删除组件 + const removeResult = await ComponentProxy.remove({ path: `${nodePath}/cc.Label` }); + expect(removeResult).toBe(true); + } catch (e) { + console.log(`createComponent test error: ${e}`); + throw e; + } + }); + + it('create - 创建不存在组件应抛出异常', async () => { + const options: IAddComponentOptions = { + nodePath: nodePath, + component: 'cc.NonExistentComponent', + }; + try { + await createComponent(options); + } catch (e) { + expect(e).toBeDefined(); + } + }); + }); + + describe('2. reset - 重置组件测试', () => { + let componentPath = ''; + beforeAll(async () => { + const addComponentInfo: IAddComponentOptions = { + nodePath: nodePath, + component: 'cc.Label', + }; + const component = await ComponentProxy.add(addComponentInfo); + componentPath = component.path; + }); + afterAll(async () => { + await ComponentProxy.remove({ path: componentPath }); + }); + + it('reset - 修改属性后重置应恢复默认值', async () => { + // 先修改属性 + const setComponentProperty: ISetPropertyOptionsInfo = { + componentPath: componentPath, + properties: { string: 'modified' }, + }; + const setResult = await ComponentProxy.setProperty(setComponentProperty); + expect(setResult).toBe(true); + + // 确认属性已修改 + let componentInfo = await ComponentProxy.query({ path: componentPath }) as IComponentInfo; + expect(componentInfo?.properties['string'].value).toBe('modified'); + + // 重置组件 + const resetResult = await resetComponent({ path: componentPath }); + expect(resetResult).toBe(true); + + // 验证属性已恢复默认值 + componentInfo = await ComponentProxy.query({ path: componentPath }) as IComponentInfo; + expect(componentInfo?.properties['string'].value).toBe('label'); + }); + + it('reset - 重置不存在的组件应返回 false', async () => { + const result = await resetComponent({ + path: 'non-existent-path/cc.Label_001', + }); + expect(result).toBe(false); + }); + }); + + describe('3. executeMethod - 执行组件方法测试', () => { + let componentUuid = ''; + let componentPath = ''; + beforeAll(async () => { + const addComponentInfo: IAddComponentOptions = { + nodePath: nodePath, + component: 'cc.Label', + }; + const component = await ComponentProxy.add(addComponentInfo); + componentUuid = component.uuid; + componentPath = component.path; + }); + afterAll(async () => { + await ComponentProxy.remove({ path: componentPath }); + }); + + it('executeMethod - 执行组件上存在的方法', async () => { + try { + await executeComponentMethod({ + path: componentPath, + name: 'onLoad', + args: [], + }); + } catch (e) { + // 某些方法可能在编辑器环境中无法执行,记录但不影响测试 + console.log(`executeComponentMethod test: ${e}`); + } + }); + + it('executeMethod - 执行返回非 undefined 值的方法', async () => { + const result = await executeComponentMethod({ + path: componentPath, + name: 'node.getSiblingIndex', + args: [], + }); + expect(result).toBeDefined(); + expect(typeof result).toBe('number'); + }); + }); + + describe('4. queryClasses - 查询注册类名测试', () => { + it('queryClasses - 无参数查询所有注册类', async () => { + const result = await queryClasses(); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + // 每个元素都有 name 字段 + for (const cls of result) { + expect(typeof cls.name).toBe('string'); + expect(cls.name.length).toBeGreaterThan(0); + } + }); + + it('queryClasses - 使用 extends 过滤子类', async () => { + const options: IQueryClassesOptions = { + extends: 'cc.Component', + }; + const result = await queryClasses(options); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + // 应该包含 cc.Label 等组件 + const names = result.map((cls) => cls.name); + expect(names).toContain('cc.Label'); + }); + + it('queryClasses - extends 数组过滤', async () => { + const options: IQueryClassesOptions = { + extends: ['cc.Component'], + }; + const result = await queryClasses(options); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + }); + + it('queryClasses - excludeSelf 排除自身', async () => { + const withSelf = await queryClasses({ extends: 'cc.Component' }); + const withoutSelf = await queryClasses({ extends: 'cc.Component', excludeSelf: true }); + expect(withSelf).toBeDefined(); + expect(withoutSelf).toBeDefined(); + + const withSelfNames = withSelf.map((cls) => cls.name); + const withoutSelfNames = withoutSelf.map((cls) => cls.name); + + // excludeSelf 应排除 cc.Component 自身 + expect(withSelfNames).toContain('cc.Component'); + expect(withoutSelfNames).not.toContain('cc.Component'); + }); + + it('queryClasses - 不存在的父类返回空数组', async () => { + const options: IQueryClassesOptions = { + extends: 'cc.NonExistentClass', + }; + const result = await queryClasses(options); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(0); + }); + }); + + describe('5. queryFunctionOfNode - 查询节点组件函数测试', () => { + let componentPath = ''; + + beforeAll(async () => { + const addComponentInfo: IAddComponentOptions = { + nodePath: nodePath, + component: 'cc.Label', + }; + const component = await ComponentProxy.add(addComponentInfo); + componentPath = component.path; + }); + afterAll(async () => { + await ComponentProxy.remove({ path: componentPath }); + }); + + it('queryFunctionOfNode - 查询有效节点的组件函数', async () => { + const result = await queryComponentFunctionOfNode(nodePath); + expect(result).toBeDefined(); + expect(typeof result).toBe('object'); + }); + + it('queryFunctionOfNode - 查询不存在节点返回空对象', async () => { + const result = await queryComponentFunctionOfNode('non-existent-path'); + expect(result).toBeDefined(); + expect(typeof result).toBe('object'); + expect(Object.keys(result).length).toBe(0); + }); + }); + + describe('6. hasScript - 查询组件是否存在脚本测试', () => { + it('hasScript - 内置组件应返回 true', async () => { + const result = await queryComponentHasScript('cc.Label'); + expect(result).toBe(true); + }); + + it('hasScript - 另一个内置组件应返回 true', async () => { + const result = await queryComponentHasScript('cc.Sprite'); + expect(result).toBe(true); + }); + + it('hasScript - 不存在的组件应返回 false', async () => { + const result = await queryComponentHasScript('cc.NonExistentComponent'); + expect(result).toBe(false); + }); + + it('hasScript - 空字符串应返回 false', async () => { + const result = await queryComponentHasScript(''); + expect(result).toBe(false); + }); + }); + + describe('7. query - 返回 IComponent 原始数据', () => { + let componentPath = ''; + let componentUuid = ''; + + beforeAll(async () => { + const component = await ComponentProxy.add({ + nodePath: nodePath, + component: 'cc.Label', + }); + componentPath = component.path; + componentUuid = component.uuid; + }); + afterAll(async () => { + await ComponentProxy.remove({ path: componentPath }); + }); + + it('query - 通过 componentPath 返回 IComponent', async () => { + const result = await queryComponentDump(componentPath); + expect(result).not.toBeNull(); + expect(result!.value).toBeDefined(); + expect(typeof result!.value).toBe('object'); + expect(result!.type).toBe('cc.Label'); + expect(result!.cid).toBe('cc.Label'); + const value = result!.value as Record; + expect(value['uuid']).toBeDefined(); + expect(value['name']).toBeDefined(); + expect(value['enabled']).toBeDefined(); + }); + + it('query - 通过 uuid 返回 IComponent', async () => { + const result = await queryComponentDump(componentUuid); + expect(result).not.toBeNull(); + expect(result!.value).toBeDefined(); + expect(typeof result!.value).toBe('object'); + expect(result!.type).toBe('cc.Label'); + expect(result!.cid).toBe('cc.Label'); + }); + + // it('query - 不存在路径返回 null', async () => { + // const result = await queryComponentDump(`${nodePath}/cc.NonExistent`); + // expect(result).toBeNull(); + // }); + + // it('query - 不存在 uuid 返回 null', async () => { + // const result = await queryComponentDump('00000000-0000-0000-0000-000000000000'); + // expect(result).toBeNull(); + // }); + }); + + describe('8. IComponent 字段详细验证', () => { + let componentPath = ''; + + beforeAll(async () => { + const component = await ComponentProxy.add({ + nodePath: nodePath, + component: 'cc.Label', + }); + componentPath = component.path; + }); + afterAll(async () => { + await ComponentProxy.remove({ path: componentPath }); + }); + + it('value 包含 uuid/name/enabled 编码后的 IProperty 结构', async () => { + const result = await queryComponentDump(componentPath); + expect(result).not.toBeNull(); + const value = result!.value as Record; + + expect(value['uuid']).toBeDefined(); + expect(value['uuid'].value).toBeDefined(); + expect(value['uuid'].type).toBeDefined(); + + expect(value['name']).toBeDefined(); + expect(value['name'].value).toBeDefined(); + expect(value['name'].type).toBeDefined(); + + expect(value['enabled']).toBeDefined(); + expect(value['enabled'].value).toBe(true); + expect(value['enabled'].type).toBeDefined(); + }); + + it('extends 继承链包含 cc.Component', async () => { + const result = await queryComponentDump(componentPath) as any; + expect(result).not.toBeNull(); + if (result.extends) { + expect(Array.isArray(result.extends)).toBe(true); + expect(result.extends).toContain('cc.Component'); + } + }); + + it('组件特有属性 string 是 IProperty 结构', async () => { + const result = await queryComponentDump(componentPath); + expect(result).not.toBeNull(); + const value = result!.value as Record; + const stringProp = value['string']; + expect(stringProp).toBeDefined(); + expect(stringProp.type).toBeDefined(); + expect(stringProp.value).toBeDefined(); + expect(typeof stringProp.visible).toBe('boolean'); + expect(typeof stringProp.readonly).toBe('boolean'); + }); + + it('组件特有属性 fontSize 是 Number 类型', async () => { + const result = await queryComponentDump(componentPath); + expect(result).not.toBeNull(); + const value = result!.value as Record; + const fontSizeProp = value['fontSize']; + expect(fontSizeProp).toBeDefined(); + expect(typeof fontSizeProp.value).toBe('number'); + expect(fontSizeProp.type).toBeDefined(); + }); + + it('editor 附加数据存在', async () => { + const result = await queryComponentDump(componentPath) as any; + expect(result).not.toBeNull(); + if (result.editor) { + expect(typeof result.editor).toBe('object'); + } + }); + }); + + describe('9. setProperty - ISetPropertyOptions 格式', () => { + let componentPath = ''; + let compIndex = -1; + + beforeAll(async () => { + const component = await ComponentProxy.add({ + nodePath: nodePath, + component: 'cc.Label', + }); + componentPath = component.path; + + // 查询组件索引 + const nodeTree: any = await rpcNodeRequest('queryNodeTree', [{ path: nodePath }]); + const compDump = await queryComponentDump(componentPath); + const compUuid = (compDump?.value as any)?.uuid?.value; + compIndex = nodeTree.components.findIndex((c: any) => c.value === compUuid); + }); + afterAll(async () => { + await ComponentProxy.remove({ path: componentPath }); + }); + + it('setProperty - 通过编辑器格式设置组件 string 属性', async () => { + const compDump = await queryComponentDump(componentPath); + const value = compDump!.value as Record; + const stringDump = { ...value['string'], value: 'editor-format-test' }; + + const result = await setComponentPropertyForEditor({ + nodePath: nodePath, + path: `__comps__.${compIndex}.string`, + dump: stringDump, + record: false, + }); + expect(result).toBe(true); + + // 验证修改生效 + const updated = await queryComponentDump(componentPath); + const updatedValue = updated!.value as Record; + expect(updatedValue['string'].value).toBe('editor-format-test'); + }); + + it('setProperty - 通过编辑器格式设置组件 fontSize 属性', async () => { + const compDump = await queryComponentDump(componentPath); + const value = compDump!.value as Record; + const fontSizeDump = { ...value['fontSize'], value: 72 }; + + const result = await setComponentPropertyForEditor({ + nodePath: nodePath, + path: `__comps__.${compIndex}.fontSize`, + dump: fontSizeDump, + record: false, + }); + expect(result).toBe(true); + + const updated = await queryComponentDump(componentPath); + const updatedValue = updated!.value as Record; + expect(updatedValue['fontSize'].value).toBe(72); + }); + + it('setProperty - 不存在节点返回 false', async () => { + const compDump = await queryComponentDump(componentPath); + const value = compDump!.value as Record; + + const result = await setComponentPropertyForEditor({ + nodePath: 'non-existent-path', + path: `__comps__.0.string`, + dump: value['string'], + record: false, + }); + expect(result).toBe(false); + }); + }); +}); diff --git a/src/core/scene/test/component-proxy.testcase.ts b/src/core/scene/test/component-proxy.testcase.ts index 7cc971b82..79d6d8fad 100644 --- a/src/core/scene/test/component-proxy.testcase.ts +++ b/src/core/scene/test/component-proxy.testcase.ts @@ -5,14 +5,12 @@ import { IAddComponentOptions, IRemoveComponentOptions, IQueryComponentOptions, - ISetPropertyOptions, IComponentIdentifier, - IComponent, - IComponentForEditor, - IQueryClassesOptions, + IComponentInfo, NodeType, - INode + INodeInfo } from '../common'; +import { ISetPropertyOptionsInfo } from '../common/cli/component'; import { ComponentProxy } from '../main-process/proxy/component-proxy'; import { NodeProxy } from '../main-process/proxy/node-proxy'; import { EditorProxy } from '../main-process/proxy/editor-proxy'; @@ -37,7 +35,7 @@ describe('Component Proxy 测试', () => { nodeType: NodeType.EMPTY, position: { x: 1, y: 2, z: 0 }, }; - const testNode = await NodeProxy.createNodeByType(params); + const testNode = await NodeProxy.createByType(params); expect(testNode).toBeDefined(); expect(testNode?.name).toBe('New Node'); if (!testNode) { @@ -54,7 +52,7 @@ describe('Component Proxy 测试', () => { path: nodePath, keepWorldTransform: false }; - await NodeProxy.deleteNode(params); + await NodeProxy.delete(params); expect(params).toBeDefined(); expect(params?.path).toBe(nodePath); } catch (e) { @@ -66,57 +64,57 @@ describe('Component Proxy 测试', () => { describe('1. 基础组件操作- 添加,查询,设置属性,移除', () => { let componentPath = ''; - let componentInfo: IComponent | null; - it('addComponent - 添加节点 - 完整节点名称:cc.Label', async () => { + let componentInfo: IComponentInfo | null; + it('add - 添加节点 - 完整节点名称:cc.Label', async () => { //console.log("Created prefab node path=", prefabNode?.path); const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: 'cc.Label' }; try { - componentInfo = await ComponentProxy.addComponent(addComponentInfo); + componentInfo = await ComponentProxy.add(addComponentInfo); componentPath = componentInfo.path; expect(componentInfo.path).toBe(`${nodePath}/cc.Label`); // 删除当前添加的节点,方便后续测试 const removeComponentInfo: IRemoveComponentOptions = { path: componentPath }; - const result = await ComponentProxy.removeComponent(removeComponentInfo); + const result = await ComponentProxy.remove(removeComponentInfo); expect(result).toBe(true); } catch (e) { console.log(`addComponent test error: ${e}`); throw e; } }); - it('addComponent - 添加节点 - 模糊节点名称:cc.label', async () => { + it('add -添加节点 - 模糊节点名称:cc.label', async () => { //console.log("Created prefab node path=", prefabNode?.path); const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: 'cc.label' }; try { - componentInfo = await ComponentProxy.addComponent(addComponentInfo); + componentInfo = await ComponentProxy.add(addComponentInfo); componentPath = componentInfo.path; expect(componentInfo.path).toBe(`${nodePath}/cc.Label`); // 删除当前添加的节点,方便后续测试 const removeComponentInfo: IRemoveComponentOptions = { path: componentPath }; - const result = await ComponentProxy.removeComponent(removeComponentInfo); + const result = await ComponentProxy.remove(removeComponentInfo); expect(result).toBe(true); } catch (e) { console.log(`addComponent test error: ${e}`); throw e; } }); - it('addComponent - 添加节点 - 模糊节点名称:Label', async () => { + it('add -添加节点 - 模糊节点名称:Label', async () => { //console.log("Created prefab node path=", prefabNode?.path); const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: 'Label' }; try { - componentInfo = await ComponentProxy.addComponent(addComponentInfo); + componentInfo = await ComponentProxy.add(addComponentInfo); componentPath = componentInfo.path; expect(componentInfo.path).toBe(`${nodePath}/cc.Label`); @@ -124,21 +122,21 @@ describe('Component Proxy 测试', () => { const removeComponentInfo: IRemoveComponentOptions = { path: componentPath }; - const result = await ComponentProxy.removeComponent(removeComponentInfo); + const result = await ComponentProxy.remove(removeComponentInfo); expect(result).toBe(true); } catch (e) { console.log(`addComponent test error: ${e}`); throw e; } }); - it('addComponent - 添加节点 - 模糊节点名称:label', async () => { + it('add -添加节点 - 模糊节点名称:label', async () => { //console.log("Created prefab node path=", prefabNode?.path); const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: 'label' }; try { - componentInfo = await ComponentProxy.addComponent(addComponentInfo); + componentInfo = await ComponentProxy.add(addComponentInfo); componentPath = componentInfo.path; expect(componentInfo.path).toBe(`${nodePath}/cc.Label`); @@ -149,12 +147,12 @@ describe('Component Proxy 测试', () => { } }); - it('queryComponent - 查询组件- 根据 uuid 查询', async () => { + it('query - 查询组件- 根据 uuid 查询', async () => { const queryComponent: IQueryComponentOptions = { path: componentInfo!.uuid }; try { - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo).toBeDefined(); if (componentInfo!.cid) { expect(componentInfo!.cid).toBe('cc.Label'); @@ -170,12 +168,12 @@ describe('Component Proxy 测试', () => { throw e; } }); - it('queryComponent - 查询组件-根据完整组件名查询', async () => { + it('query - 查询组件-根据完整组件名查询', async () => { const queryComponent: IQueryComponentOptions = { path: componentPath }; try { - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo).toBeDefined(); if (componentInfo!.cid) { expect(componentInfo!.cid).toBe('cc.Label'); @@ -191,12 +189,12 @@ describe('Component Proxy 测试', () => { throw e; } }); - it('queryComponent - 查询组件-根据模糊的匹配-Label', async () => { + it('query - 查询组件-根据模糊的匹配-Label', async () => { const queryComponent: IQueryComponentOptions = { path: nodePath + '/Label' }; try { - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo).toBeDefined(); if (componentInfo!.cid) { expect(componentInfo!.cid).toBe('cc.Label'); @@ -212,12 +210,12 @@ describe('Component Proxy 测试', () => { throw e; } }); - it('queryComponent - 查询组件-根据模糊的匹配-cc.label', async () => { + it('query - 查询组件-根据模糊的匹配-cc.label', async () => { const queryComponent: IQueryComponentOptions = { path: nodePath + '/cc.label' }; try { - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo).toBeDefined(); if (componentInfo!.cid) { expect(componentInfo!.cid).toBe('cc.Label'); @@ -233,12 +231,12 @@ describe('Component Proxy 测试', () => { throw e; } }); - it('queryComponent - 查询组件-根据模糊的匹配-Label', async () => { + it('query - 查询组件-根据模糊的匹配-Label', async () => { const queryComponent: IQueryComponentOptions = { path: nodePath + '/Label' }; try { - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo).toBeDefined(); if (componentInfo!.cid) { expect(componentInfo!.cid).toBe('cc.Label'); @@ -254,12 +252,12 @@ describe('Component Proxy 测试', () => { throw e; } }); - it('queryComponent - 查询组件-根据模糊的匹配-label', async () => { + it('query - 查询组件-根据模糊的匹配-label', async () => { const queryComponent: IQueryComponentOptions = { path: nodePath + '/label' }; try { - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo).toBeDefined(); if (componentInfo!.cid) { expect(componentInfo!.cid).toBe('cc.Label'); @@ -276,12 +274,12 @@ describe('Component Proxy 测试', () => { } }); - it('queryComponent - 查询组件-根据模糊的匹配-label不带下标', async () => { + it('query - 查询组件-根据模糊的匹配-label不带下标', async () => { const queryComponent: IQueryComponentOptions = { path: nodePath + '/label' }; try { - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo).toBeDefined(); if (componentInfo!.cid) { expect(componentInfo!.cid).toBe('cc.Label'); @@ -297,12 +295,12 @@ describe('Component Proxy 测试', () => { throw e; } }); - it('queryComponent - 查询组件-根据模糊的匹配-cc.label不带下标', async () => { + it('query - 查询组件-根据模糊的匹配-cc.label不带下标', async () => { const queryComponent: IQueryComponentOptions = { path: nodePath + '/cc.label' }; try { - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo).toBeDefined(); if (componentInfo!.cid) { expect(componentInfo!.cid).toBe('cc.Label'); @@ -318,23 +316,23 @@ describe('Component Proxy 测试', () => { throw e; } }); - it('queryComponent - 查询不存在组件', async () => { + it('query - 查询不存在组件', async () => { const queryComponent: IQueryComponentOptions = { path: nodePath + '/cc.Button' }; try { - await ComponentProxy.queryComponent(queryComponent) as IComponent; + await ComponentProxy.query(queryComponent) as IComponentInfo; } catch (e) { expect(e instanceof Error ? e.message : String(e)).toBe(`No component found for this path(${queryComponent.path}).`); } }); - it('queryComponent - 根据不存在的 URL 查询组件', async () => { + it('query - 根据不存在的 URL 查询组件', async () => { const queryComponent: IQueryComponentOptions = { path: 'db://assets/non-existent-script.ts' }; try { - const result = await ComponentProxy.queryComponent(queryComponent) as IComponent; + const result = await ComponentProxy.query(queryComponent) as IComponentInfo; // 如果没有抛出异常,则结果应该为 null expect(result).toBeNull(); } catch (e) { @@ -343,10 +341,10 @@ describe('Component Proxy 测试', () => { } }); - it('queryComponent - 查询存在相同组件', async () => { + it('query - 查询存在相同组件', async () => { const newNodePath = 'TestNode/new node'; const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: newNodePath, + nodePath: newNodePath, component: 'label' }; try { @@ -356,21 +354,20 @@ describe('Component Proxy 测试', () => { nodeType: NodeType.EMPTY, position: { x: 1, y: 2, z: 0 }, }; - const testNode = await NodeProxy.createNodeByType(params); + const testNode = await NodeProxy.createByType(params); expect(testNode).toBeDefined(); expect(testNode?.name).toBe('new node'); if (!testNode) { return; } - const cameraComponentInfo = await ComponentProxy.addComponent(addComponentInfo); - componentPath = cameraComponentInfo.path; - expect(cameraComponentInfo.path).toBe(`${addComponentInfo.nodePathOrUuid}/cc.Label`); + const cameraComponentInfo = await ComponentProxy.add(addComponentInfo); + expect(cameraComponentInfo.path).toBe(`${addComponentInfo.nodePath}/cc.Label`); const queryComponent: IQueryComponentOptions = { path: nodePath + '/cc.label' }; - await ComponentProxy.queryComponent(queryComponent) as IComponent; + await ComponentProxy.query(queryComponent) as IComponentInfo; } catch (e) { expect(e instanceof Error ? e.message : String(e)).toBe(`This path contains multiple component paths(TestNode/New Node/cc.Label,TestNode/new node/cc.Label). Please specify which one to use.`); @@ -379,17 +376,17 @@ describe('Component Proxy 测试', () => { const removeComponentInfo: IRemoveComponentOptions = { path: `${newNodePath}/cc.Label` }; - const result = await ComponentProxy.removeComponent(removeComponentInfo); + const result = await ComponentProxy.remove(removeComponentInfo); expect(result).toBe(true); } }); - it('setComponentProperty - 设置组件属性 - string类型', async () => { + it('setProperty - 设置组件属性 - string类型', async () => { const queryComponent: IQueryComponentOptions = { path: componentPath }; try { - const setComponentProperty: ISetPropertyOptions = { + const setComponentProperty: ISetPropertyOptionsInfo = { componentPath: componentPath, properties: { string: 'abc', @@ -398,7 +395,7 @@ describe('Component Proxy 测试', () => { expect(componentInfo?.properties['string'].value).toBe('label'); const result = await ComponentProxy.setProperty(setComponentProperty); expect(result).toBe(true); - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo?.properties['string'].value).toBe('abc'); } catch (e) { console.log(`setComponentProperty test error: ${e}`); @@ -406,12 +403,12 @@ describe('Component Proxy 测试', () => { } }); - it('removeComponent - 删除组件', async () => { + it('remove - 删除组件', async () => { const removeComponentInfo: IRemoveComponentOptions = { path: componentPath }; try { - const result = await ComponentProxy.removeComponent(removeComponentInfo); + const result = await ComponentProxy.remove(removeComponentInfo); expect(result).toBe(true); } catch (e) { console.log(`removeComponent test error: ${e}`); @@ -422,7 +419,7 @@ describe('Component Proxy 测试', () => { path: componentPath }; try { - await ComponentProxy.queryComponent(queryComponent) as IComponent; + await ComponentProxy.query(queryComponent) as IComponentInfo; } catch (e) { expect(e instanceof Error ? e.message : String(e)).toBe(`No component found for this path(${queryComponent.path}).`); } @@ -436,7 +433,7 @@ describe('Component Proxy 测试', () => { afterAll(async () => { try { for (const component of components) { - const result = await ComponentProxy.removeComponent({ path: component.path }); + const result = await ComponentProxy.remove({ path: component.path }); expect(result).toBe(true); }; } catch (e) { @@ -445,18 +442,18 @@ describe('Component Proxy 测试', () => { } console.log('组合测试 - 添加多个不同节点 - 结束'); }); - it('addComponent - 添加多个不同节点', async () => { + it('add -添加多个不同节点', async () => { try { for (const componentName of testComponents) { const componentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: componentName }; - const component = await ComponentProxy.addComponent(componentInfo); + const component = await ComponentProxy.add(componentInfo); expect(component.path).toBe(`${nodePath}/${componentName}`); components.push(component); - const queryComponentInfo = await ComponentProxy.queryComponent({ path: component.path }) as IComponent; + const queryComponentInfo = await ComponentProxy.query({ path: component.path }) as IComponentInfo; if (queryComponentInfo!.cid) { expect(queryComponentInfo!.cid).toBe(componentName); } @@ -479,7 +476,7 @@ describe('Component Proxy 测试', () => { afterAll(async () => { try { for (const component of components) { - const result = await ComponentProxy.removeComponent({ path: component.path }); + const result = await ComponentProxy.remove({ path: component.path }); expect(result).toBe(true); }; } catch (e) { @@ -488,17 +485,17 @@ describe('Component Proxy 测试', () => { } console.log('组合测试 - 添加多个相同节点 - 结束'); }); - it('addComponent - 添加多个相同节点', async () => { + it('add -添加多个相同节点', async () => { try { for (let i = 0; i < testCount; i++) { const componentInfo1: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: testComponent }; - const component = await ComponentProxy.addComponent(componentInfo1); + const component = await ComponentProxy.add(componentInfo1); expect(component.path).toBe(`${nodePath}/${testComponent}${i === 0 ? '' : '_' + String(i).padStart(3, '0')}`); components.push(component); - const queryComponentInfo = await ComponentProxy.queryComponent({ path: component.path }) as IComponent; + const queryComponentInfo = await ComponentProxy.query({ path: component.path }) as IComponentInfo; if (queryComponentInfo!.cid) { expect(queryComponentInfo!.cid).toBe(testComponent); } @@ -515,20 +512,20 @@ describe('Component Proxy 测试', () => { }); describe('4. 设置组件属性测试 - 设置不同类型的属性', () => { const testComponent: string = 'cc.Label'; - let componentInfo: IComponent | null; + let componentInfo: IComponentInfo | null; let componentPath: string = ''; const queryComponent: IQueryComponentOptions = { path: '' }; // 确保测试了中,没有其他的组件 beforeAll(async () => { const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: testComponent }; try { - const component = await ComponentProxy.addComponent(addComponentInfo); + const component = await ComponentProxy.add(addComponentInfo); componentPath = component.path; expect(component.path).toBe(`${nodePath}/cc.Label`); - componentInfo = await ComponentProxy.queryComponent({ path: componentPath }) as IComponent; + componentInfo = await ComponentProxy.query({ path: componentPath }) as IComponentInfo; expect(componentInfo).toBeDefined(); queryComponent.path = componentPath; } catch (e) { @@ -537,7 +534,7 @@ describe('Component Proxy 测试', () => { }); afterAll(async () => { try { - const result = await ComponentProxy.removeComponent({ path: componentPath }); + const result = await ComponentProxy.remove({ path: componentPath }); expect(result).toBe(true); } catch (e) { console.log(`组合测试 - 添加多个相同节点 - 错误 ${e}`); @@ -545,11 +542,11 @@ describe('Component Proxy 测试', () => { } console.log('组合测试 - 添加多个相同节点 - 结束'); }); - it('setComponentProperty - 设置组件属性 - number类型', async () => { + it('setProperty - 设置组件属性 - number类型', async () => { try { expect(componentInfo?.properties['fontSize'].value).toBe(40); - const setComponentProperty: ISetPropertyOptions = { + const setComponentProperty: ISetPropertyOptionsInfo = { componentPath: componentPath, properties: { fontSize: 80, @@ -557,48 +554,48 @@ describe('Component Proxy 测试', () => { }; const result = await ComponentProxy.setProperty(setComponentProperty); expect(result).toBe(true); - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo?.properties['fontSize'].value).toBe(80); } catch (e) { console.log(`setComponentProperty test error: ${e}`); throw e; } }); - it('setComponentProperty - 设置组件属性 - enum类型', async () => { + it('setProperty - 设置组件属性 - enum类型', async () => { try { - const setComponentProperty: ISetPropertyOptions = { + const setComponentProperty: ISetPropertyOptionsInfo = { componentPath: componentPath, properties: { overflow: 1 }, }; expect(componentInfo?.properties['overflow'].value).toBe(0); const result = await ComponentProxy.setProperty(setComponentProperty); expect(result).toBe(true); - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo?.properties['overflow'].value).toBe(1); } catch (e) { console.log(`setComponentProperty test error: ${e}`); throw e; } }); - it('setComponentProperty - 设置组件属性 - boolean类型', async () => { + it('setProperty - 设置组件属性 - boolean类型', async () => { try { - const setComponentProperty: ISetPropertyOptions = { + const setComponentProperty: ISetPropertyOptionsInfo = { componentPath: componentPath, properties: { enableOutline: true }, }; expect(componentInfo?.properties['enableOutline'].value).toBe(false); const result = await ComponentProxy.setProperty(setComponentProperty); expect(result).toBe(true); - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo?.properties['enableOutline'].value).toBe(true); } catch (e) { console.log(`setComponentProperty test error: ${e}`); throw e; } }); - it('setComponentProperty - 设置组件属性 - color类型', async () => { + it('setProperty - 设置组件属性 - color类型', async () => { try { - const setComponentProperty: ISetPropertyOptions = { + const setComponentProperty: ISetPropertyOptionsInfo = { componentPath: componentPath, properties: { outlineColor: { @@ -615,7 +612,7 @@ describe('Component Proxy 测试', () => { expect(componentInfo?.properties['outlineColor'].value.a).toBe(255); const result = await ComponentProxy.setProperty(setComponentProperty); expect(result).toBe(true); - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo?.properties['outlineColor'].value.r).toBe(50); expect(componentInfo?.properties['outlineColor'].value.g).toBe(100); expect(componentInfo?.properties['outlineColor'].value.b).toBe(150); @@ -625,9 +622,9 @@ describe('Component Proxy 测试', () => { throw e; } }); - it('setComponentProperty - 设置组件属性 - 设置枚举类型之外的值', async () => { + it('setProperty - 设置组件属性 - 设置枚举类型之外的值', async () => { try { - const setComponentProperty: ISetPropertyOptions = { + const setComponentProperty: ISetPropertyOptionsInfo = { componentPath: componentPath, properties: { overflow: 100000 @@ -636,7 +633,7 @@ describe('Component Proxy 测试', () => { expect(componentInfo?.properties['overflow'].value).toBe(1); const result = await ComponentProxy.setProperty(setComponentProperty); expect(result).toBe(true); - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo?.properties['overflow'].value).toBe(100000); } catch (e) { console.log(`setComponentProperty test error: ${e}`); @@ -646,20 +643,20 @@ describe('Component Proxy 测试', () => { }); describe('4.1 设置Sprite属性测试', () => { const testComponent: string = 'cc.Sprite'; - let componentInfo: IComponent | null; + let componentInfo: IComponentInfo | null; let componentPath: string = ''; const queryComponent: IQueryComponentOptions = { path: '' }; // 确保测试了中,没有其他的组件 beforeAll(async () => { const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: testComponent }; try { - const component = await ComponentProxy.addComponent(addComponentInfo); + const component = await ComponentProxy.add(addComponentInfo); componentPath = component.path; expect(component.path).toBe(`${nodePath}/cc.Sprite`); - componentInfo = await ComponentProxy.queryComponent({ path: componentPath }) as IComponent; + componentInfo = await ComponentProxy.query({ path: componentPath }) as IComponentInfo; expect(componentInfo).toBeDefined(); queryComponent.path = componentPath; } catch (e) { @@ -669,17 +666,17 @@ describe('Component Proxy 测试', () => { }); afterAll(async () => { try { - const result = await ComponentProxy.removeComponent({ path: componentPath }); + const result = await ComponentProxy.remove({ path: componentPath }); expect(result).toBe(true); } catch (e) { console.log(`组合测试 - 添加多个相同节点 - 错误 ${e}`); throw e; } }); - it('setComponentProperty - 设置组件属性 - 设置SpriteFrame', async () => { + it('setProperty - 设置组件属性 - 设置SpriteFrame', async () => { try { // 对错误的值 类型 会修改失败,但是返回还是true - const setComponentProperty: ISetPropertyOptions = { + const setComponentProperty: ISetPropertyOptionsInfo = { componentPath: componentPath, properties: { spriteFrame: { @@ -690,7 +687,7 @@ describe('Component Proxy 测试', () => { expect(componentInfo?.properties['spriteFrame'].value.uuid).toBe(''); const result = await ComponentProxy.setProperty(setComponentProperty); expect(result).toBe(true); - componentInfo = await ComponentProxy.queryComponent(queryComponent) as IComponent; + componentInfo = await ComponentProxy.query(queryComponent) as IComponentInfo; expect(componentInfo?.properties['spriteFrame'].value.uuid).toBe('20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941'); } catch (e) { console.log(`setComponentProperty test error: ${e}`); @@ -711,13 +708,13 @@ describe('Component Proxy 测试', () => { queryChildren: false, queryComponent: true }; - buildinComponentTypes = await ComponentProxy.queryAllComponent(); - const result = await NodeProxy.queryNode(params); + buildinComponentTypes = await ComponentProxy.queryAll(); + const result = await NodeProxy.query(params) as INodeInfo | null; expect(result).toBeDefined(); expect(result?.components?.length == 0); }); - it('addComponent - 添加内置组件测试 - 这个测试例设计有问题,可以忽略。', async () => { + it('add -添加内置组件测试 - 这个测试例设计有问题,可以忽略。', async () => { /** * 这个测试例设计有问题,因为内置组件太多,有冲突,有重复(依赖创建组件 会有重复),有无法删除组件(UITransform) * 这样导致很难排除哪些有依赖,哪些有冲突等,因此,只能通过日志的方式输出,查看哪些组件是冲突的。 @@ -741,11 +738,11 @@ describe('Component Proxy 测试', () => { } const componentInfo1: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: componentType }; try { - const component = await ComponentProxy.addComponent(componentInfo1); + const component = await ComponentProxy.add(componentInfo1); createdComponents.push(component); } catch (e) { // 这里会产生冲突、重复组件(因为依赖会创建一些重复组件,导致测试会异常), 这是正常的异常 @@ -759,9 +756,9 @@ describe('Component Proxy 测试', () => { queryChildren: false, queryComponent: true }; - const node = await NodeProxy.queryNode(params); + const node = await NodeProxy.query(params) as INodeInfo | null; for (let i = 0; i < node!.components!.length; ++i) { - await ComponentProxy.removeComponent({ path: node!.components!.at(i)!.path }); + await ComponentProxy.remove({ path: node!.components!.at(i)!.path }); } } catch (e) { // 有些移除会失败,因为有依赖,例如 UITransform 、 Label组件,也属于正常的异常,这也属于正常的异常 @@ -778,7 +775,7 @@ describe('Component Proxy 测试', () => { }); describe('6. 多节点添加同组件-组件不冲突', () => { const testCount = 10; - const nodes: INode[] = []; + const nodes: INodeInfo[] = []; beforeAll(async () => { for (let i = 0; i < testCount; ++i) { const params: ICreateByNodeTypeParams = { @@ -786,7 +783,7 @@ describe('Component Proxy 测试', () => { nodeType: NodeType.EMPTY, position: { x: 1, y: 2, z: 0 }, }; - const testNode = await NodeProxy.createNodeByType(params); + const testNode = await NodeProxy.createByType(params); expect(testNode).toBeDefined(); if (!testNode) { return; @@ -800,29 +797,29 @@ describe('Component Proxy 测试', () => { path: nodes[i].path, keepWorldTransform: false }; - await NodeProxy.deleteNode(params); + await NodeProxy.delete(params); expect(params).toBeDefined(); } }); - it('addComponent - 每个组件添加同一个组件,但是最后的组件名是一样的,只是节点名称不一样', async () => { + it('add -每个组件添加同一个组件,但是最后的组件名是一样的,只是节点名称不一样', async () => { try { const testComponent = 'cc.Layout'; for (let i = 0; i < nodes.length; ++i) { const componentInfo1: IAddComponentOptions = { - nodePathOrUuid: nodes[i].path, + nodePath: nodes[i].path, component: testComponent, }; - const component = await ComponentProxy.addComponent(componentInfo1); + const component = await ComponentProxy.add(componentInfo1); expect(component).toBeDefined(); expect(component.path).toBe(`${nodes[i].path}/cc.Layout`); } for (let i = 0; i < nodes.length; ++i) { const componentInfo1: IAddComponentOptions = { - nodePathOrUuid: nodes[i].path, + nodePath: nodes[i].path, component: testComponent, }; - const component = await ComponentProxy.addComponent(componentInfo1); + const component = await ComponentProxy.add(componentInfo1); expect(component).toBeDefined(); expect(component.path).toBe(`${nodes[i].path}/cc.Layout_001`); } @@ -842,7 +839,7 @@ describe('Component Proxy 测试', () => { nodeType: NodeType.EMPTY, position: { x: 1, y: 2, z: 0 }, }; - const testNode = await NodeProxy.createNodeByType(params); + const testNode = await NodeProxy.createByType(params); expect(testNode).toBeDefined(); if (!testNode) { return; @@ -855,45 +852,45 @@ describe('Component Proxy 测试', () => { path: nodePath, keepWorldTransform: false }; - await NodeProxy.deleteNode(params); + await NodeProxy.delete(params); expect(params).toBeDefined(); }); - it('addComponent - 添加多个不允许并存的组件', async () => { + it('add -添加多个不允许并存的组件', async () => { const testComponent = 'cc.Label'; const componentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: testComponent, }; - let component = await ComponentProxy.addComponent(componentInfo); + let component = await ComponentProxy.add(componentInfo); expect(component).toBeDefined(); expect(component.path).toBe(`${nodePath}/${testComponent}`); try { - component = await ComponentProxy.addComponent(componentInfo); + component = await ComponentProxy.add(componentInfo); } catch (e) { // 添加接受相同组件添加的错误 expect(e instanceof Error ? e.message : String(e)).toBe(`Can't add component '${testComponent}' because ${nodeName} already contains the same component.`); expect(component.path).toBe(`${nodePath}/${testComponent}`); } - const result = await ComponentProxy.removeComponent({ path: component.path }); + const result = await ComponentProxy.remove({ path: component.path }); expect(result).toBe(true); }); - it('addComponent - 添加多个冲突的组件', async () => { + it('add -添加多个冲突的组件', async () => { const testComponent = 'cc.Sprite'; const testConfictsComponent = 'cc.Line'; const componentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: testComponent, }; - let component = await ComponentProxy.addComponent(componentInfo); + let component = await ComponentProxy.add(componentInfo); expect(component).toBeDefined(); expect(component.path).toBe(`${nodePath}/${testComponent}`); try { const componentConficts: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: testConfictsComponent, }; - component = await ComponentProxy.addComponent(componentConficts); + component = await ComponentProxy.add(componentConficts); } catch (e) { // 添加异常冲突 expect(e instanceof Error ? e.message : String(e)).toBe(`Can't add component '${testConfictsComponent}' to ${nodeName} because it conflicts with the existing '${testComponent}' derived component.`); @@ -902,60 +899,29 @@ describe('Component Proxy 测试', () => { }); }); - describe('8. createComponent - 创建组件测试', () => { - it('createComponent - 创建已知组件应返回 true', async () => { - const options: IAddComponentOptions = { - nodePathOrUuid: nodePath, - component: 'cc.Label', - }; - try { - const result = await ComponentProxy.createComponent(options); - expect(result).toBe(true); - // 删除组件 - const removeResult = await ComponentProxy.removeComponent({ path: `${nodePath}/cc.Label` }); - expect(removeResult).toBe(true); - } catch (e) { - console.log(`createComponent test error: ${e}`); - throw e; - } - }); - - it('createComponent - 创建不存在组件应抛出异常', async () => { - const options: IAddComponentOptions = { - nodePathOrUuid: nodePath, - component: 'cc.NonExistentComponent', - }; - try { - await ComponentProxy.createComponent(options); - } catch (e) { - expect(e).toBeDefined(); - } - }); - }); - describe('9. queryComponent - cli传IQueryComponentOptions,editor直接传uuid', () => { + describe('9. query - IComponentInfo 结构验证', () => { let componentPath = ''; let componentUuid = ''; beforeAll(async () => { const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: 'cc.Label', }; - const component = await ComponentProxy.addComponent(addComponentInfo); + const component = await ComponentProxy.add(addComponentInfo); componentPath = component.path; componentUuid = component.uuid; }); afterAll(async () => { - await ComponentProxy.removeComponent({ path: componentPath }); + await ComponentProxy.remove({ path: componentPath }); }); - it('queryComponent - cli 返回 IComponent 结构', async () => { + it('query - cli 返回 IComponentInfo 结构', async () => { const params: IQueryComponentOptions = { path: componentPath, }; - const result = await ComponentProxy.queryComponent(params) as IComponent; + const result = await ComponentProxy.query(params) as IComponentInfo; expect(result).toBeDefined(); - // IComponent 有 properties、path、uuid、name、enabled 等直接值字段 expect(result.properties).toBeDefined(); expect(typeof result.properties).toBe('object'); expect(result.path).toBeDefined(); @@ -964,258 +930,57 @@ describe('Component Proxy 测试', () => { expect(typeof result.enabled).toBe('boolean'); expect(result.cid).toBe('cc.Label'); }); - it('queryComponent - 返回 IComponentForEditor 结构', async () => { - const result = await ComponentProxy.queryComponent(componentPath) as IComponentForEditor; - expect(result).toBeDefined(); - // IComponentForEditor 有 value(对象,包含编码后的属性)、type、cid、mountedRoot 等字段 - expect(result.value).toBeDefined(); - expect(typeof result.value).toBe('object'); - expect(result.type).toBe('cc.Label'); - expect(result.cid).toBe('cc.Label'); - // value 中包含 uuid、name、enabled 等编码后的属性 - if (result.value && typeof result.value === 'object' && !Array.isArray(result.value)) { - const value = result.value as Record; - expect(value['uuid']).toBeDefined(); - expect(value['name']).toBeDefined(); - expect(value['enabled']).toBeDefined(); - } - }); - }); - - describe('10. resetComponent - 重置组件测试', () => { - let componentPath = ''; - beforeAll(async () => { - const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, - component: 'cc.Label', - }; - const component = await ComponentProxy.addComponent(addComponentInfo); - componentPath = component.path; - }); - afterAll(async () => { - await ComponentProxy.removeComponent({ path: componentPath }); - }); - - it('resetComponent - 修改属性后重置应恢复默认值', async () => { - // 先修改属性 - const setComponentProperty: ISetPropertyOptions = { - componentPath: componentPath, - properties: { string: 'modified' }, - }; - const setResult = await ComponentProxy.setProperty(setComponentProperty); - expect(setResult).toBe(true); - // 确认属性已修改 - let componentInfo = await ComponentProxy.queryComponent({ path: componentPath }) as IComponent; - expect(componentInfo?.properties['string'].value).toBe('modified'); - - // 重置组件 - const resetResult = await ComponentProxy.resetComponent({ path: componentPath }); - expect(resetResult).toBe(true); - - // 验证属性已恢复默认值 - componentInfo = await ComponentProxy.queryComponent({ path: componentPath }) as IComponent; - expect(componentInfo?.properties['string'].value).toBe('label'); - }); - - it('resetComponent - 重置不存在的组件应返回 false', async () => { - const result = await ComponentProxy.resetComponent({ - path: 'non-existent-path/cc.Label_001', - }); - expect(result).toBe(false); - }); - }); - - describe('11. executeComponentMethod - 执行组件方法测试', () => { - let componentUuid = ''; - let componentPath = ''; - beforeAll(async () => { - const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, - component: 'cc.Label', - }; - const component = await ComponentProxy.addComponent(addComponentInfo); - componentUuid = component.uuid; - componentPath = component.path; - }); - afterAll(async () => { - await ComponentProxy.removeComponent({ path: componentPath }); - }); - - it('executeComponentMethod - 执行组件上存在的方法', async () => { - try { - const result = await ComponentProxy.executeComponentMethod({ - uuid: componentUuid, - name: 'onLoad', - args: [], - }); - expect(typeof result).toBe('boolean'); - } catch (e) { - // 某些方法可能在编辑器环境中无法执行,记录但不影响测试 - console.log(`executeComponentMethod test: ${e}`); - } - }); - }); - - describe('12. queryClasses - 查询注册类名测试', () => { - it('queryClasses - 无参数查询所有注册类', async () => { - const result = await ComponentProxy.queryClasses(); - expect(result).toBeDefined(); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBeGreaterThan(0); - // 每个元素都有 name 字段 - for (const cls of result) { - expect(typeof cls.name).toBe('string'); - expect(cls.name.length).toBeGreaterThan(0); - } - }); - - it('queryClasses - 使用 extends 过滤子类', async () => { - const options: IQueryClassesOptions = { - extends: 'cc.Component', - }; - const result = await ComponentProxy.queryClasses(options); + it('query - IComponentInfo 字段完整性验证', async () => { + const result = await ComponentProxy.query({ path: componentPath }) as IComponentInfo; expect(result).toBeDefined(); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBeGreaterThan(0); - // 应该包含 cc.Label 等组件 - const names = result.map((cls) => cls.name); - expect(names).toContain('cc.Label'); - }); - - it('queryClasses - extends 数组过滤', async () => { - const options: IQueryClassesOptions = { - extends: ['cc.Component'], - }; - const result = await ComponentProxy.queryClasses(options); - expect(result).toBeDefined(); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBeGreaterThan(0); - }); - - it('queryClasses - excludeSelf 排除自身', async () => { - const withSelf = await ComponentProxy.queryClasses({ extends: 'cc.Component' }); - const withoutSelf = await ComponentProxy.queryClasses({ extends: 'cc.Component', excludeSelf: true }); - expect(withSelf).toBeDefined(); - expect(withoutSelf).toBeDefined(); - - const withSelfNames = withSelf.map((cls) => cls.name); - const withoutSelfNames = withoutSelf.map((cls) => cls.name); - - // excludeSelf 应排除 cc.Component 自身 - expect(withSelfNames).toContain('cc.Component'); - expect(withoutSelfNames).not.toContain('cc.Component'); - }); - - it('queryClasses - 不存在的父类返回空数组', async () => { - const options: IQueryClassesOptions = { - extends: 'cc.NonExistentClass', - }; - const result = await ComponentProxy.queryClasses(options); - expect(result).toBeDefined(); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(0); - }); - }); - - describe('13. queryComponentFunctionOfNode - 查询节点组件函数测试', () => { - let componentPath = ''; - - beforeAll(async () => { - const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, - component: 'cc.Label', - }; - const component = await ComponentProxy.addComponent(addComponentInfo); - componentPath = component.path; - }); - afterAll(async () => { - await ComponentProxy.removeComponent({ path: componentPath }); - }); - - it('queryComponentFunctionOfNode - 查询有效节点的组件函数', async () => { - const result = await ComponentProxy.queryComponentFunctionOfNode(nodeId); - expect(result).toBeDefined(); - expect(typeof result).toBe('object'); - }); - - it('queryComponentFunctionOfNode - 查询不存在节点返回空对象', async () => { - const result = await ComponentProxy.queryComponentFunctionOfNode('non-existent-uuid'); - expect(result).toBeDefined(); - expect(typeof result).toBe('object'); - expect(Object.keys(result).length).toBe(0); + expect(result.cid).toBe('cc.Label'); + expect(result.path).toBe(componentPath); + expect(result.uuid).toBe(componentUuid); + expect(typeof result.name).toBe('string'); + expect(result.type).toBe('cc.Label'); + expect(typeof result.enabled).toBe('boolean'); + expect(typeof result.properties).toBe('object'); + expect(result.prefab).toBeNull(); }); }); - describe('14. queryComponentHasScript - 查询组件是否存在脚本测试', () => { - it('queryComponentHasScript - 内置组件应返回 true', async () => { - const result = await ComponentProxy.queryComponentHasScript('cc.Label'); - expect(result).toBe(true); - }); - - it('queryComponentHasScript - 另一个内置组件应返回 true', async () => { - const result = await ComponentProxy.queryComponentHasScript('cc.Sprite'); - expect(result).toBe(true); - }); - - it('queryComponentHasScript - 不存在的组件应返回 false', async () => { - const result = await ComponentProxy.queryComponentHasScript('cc.NonExistentComponent'); - expect(result).toBe(false); - }); - - it('queryComponentHasScript - 空字符串应返回 false', async () => { - const result = await ComponentProxy.queryComponentHasScript(''); - expect(result).toBe(false); - }); - }); - describe('15. setPropertyForEditor - Editor 专属设置属性测试', () => { + describe('15. setProperty - record 参数测试', () => { let componentPath = ''; - let nodeUUid = ''; beforeAll(async () => { const queryNodeParam: IQueryNodeParams = { path: nodePath, queryChildren: false, queryComponent: false, }; - const nodeInfo = await NodeProxy.queryNode(queryNodeParam); + const nodeInfo = await NodeProxy.query(queryNodeParam) as INodeInfo | null; const addComponentInfo: IAddComponentOptions = { - nodePathOrUuid: nodePath, + nodePath: nodePath, component: 'cc.Label', }; - const component = await ComponentProxy.addComponent(addComponentInfo); + const component = await ComponentProxy.add(addComponentInfo); componentPath = component.path; - nodeUUid = nodeInfo!.nodeId; }); afterAll(async () => { - await ComponentProxy.removeComponent({ path: componentPath }); + await ComponentProxy.remove({ path: componentPath }); }); - it('setPropertyForEditor - 设置 string 属性', async () => { - // 先查询获取当前 dump 结构 - const fullComponent = await ComponentProxy.queryComponent(componentPath) as IComponentForEditor; - expect(fullComponent).toBeDefined(); - - if (fullComponent.value && typeof fullComponent.value === 'object' && !Array.isArray(fullComponent.value)) { - const value = fullComponent.value as Record; - const stringDump = { ...value['string'], value: 'pink-test' }; - - const result = await ComponentProxy.setProperty({ - uuid: nodeUUid, - path: '__comps__.2.string', - dump: stringDump, - record: false - }); - expect(result).toBe(true); + it('setProperty - record:false 设置 string 属性', async () => { + const result = await ComponentProxy.setProperty({ + componentPath: componentPath, + properties: { string: 'pink-test' }, + record: false + }); + expect(result).toBe(true); - // 验证修改生效 - const updated = await ComponentProxy.queryComponent({ - path: componentPath, - }) as IComponent; - expect(updated?.properties['string'].value).toBe('pink-test'); - } + // 验证修改生效 + const updated = await ComponentProxy.query({ + path: componentPath, + }) as IComponentInfo; + expect(updated?.properties['string'].value).toBe('pink-test'); }); }); @@ -1228,66 +993,66 @@ describe('Component Proxy 测试', () => { nodeType: NodeType.EMPTY, position: { x: 0, y: 0, z: 0 }, }; - const testNode = await NodeProxy.createNodeByType(params); + const testNode = await NodeProxy.createByType(params); expect(testNode).toBeDefined(); testNodePath = testNode!.path; }); afterAll(async () => { - await NodeProxy.deleteNode({ path: testNodePath, keepWorldTransform: false }); + await NodeProxy.delete({ path: testNodePath, keepWorldTransform: false }); }); - it('addComponent - 唯一组件不添加后缀', async () => { - const component = await ComponentProxy.addComponent({ - nodePathOrUuid: testNodePath, + it('add - 唯一组件不添加后缀', async () => { + const component = await ComponentProxy.add({ + nodePath: testNodePath, component: 'cc.Label', }); expect(component).toBeDefined(); expect(component.path).toBe(`${testNodePath}/cc.Label`); - await ComponentProxy.removeComponent({ path: component.path }); + await ComponentProxy.remove({ path: component.path }); }); - it('addComponent - 两个不同类型组件各自不添加后缀', async () => { - const comp1 = await ComponentProxy.addComponent({ - nodePathOrUuid: testNodePath, + it('add - 两个不同类型组件各自不添加后缀', async () => { + const comp1 = await ComponentProxy.add({ + nodePath: testNodePath, component: 'cc.Label', }); - const comp2 = await ComponentProxy.addComponent({ - nodePathOrUuid: testNodePath, + const comp2 = await ComponentProxy.add({ + nodePath: testNodePath, component: 'cc.Layout', }); expect(comp1.path).toBe(`${testNodePath}/cc.Label`); expect(comp2.path).toBe(`${testNodePath}/cc.Layout`); - await ComponentProxy.removeComponent({ path: comp2.path }); - await ComponentProxy.removeComponent({ path: comp1.path }); + await ComponentProxy.remove({ path: comp2.path }); + await ComponentProxy.remove({ path: comp1.path }); }); - it('addComponent - 第二个同类型组件添加_001后缀', async () => { - const comp1 = await ComponentProxy.addComponent({ - nodePathOrUuid: testNodePath, + it('add - 第二个同类型组件添加_001后缀', async () => { + const comp1 = await ComponentProxy.add({ + nodePath: testNodePath, component: 'cc.Layout', }); expect(comp1.path).toBe(`${testNodePath}/cc.Layout`); - const comp2 = await ComponentProxy.addComponent({ - nodePathOrUuid: testNodePath, + const comp2 = await ComponentProxy.add({ + nodePath: testNodePath, component: 'cc.Layout', }); expect(comp2.path).toBe(`${testNodePath}/cc.Layout_001`); - await ComponentProxy.removeComponent({ path: comp2.path }); - await ComponentProxy.removeComponent({ path: comp1.path }); + await ComponentProxy.remove({ path: comp2.path }); + await ComponentProxy.remove({ path: comp1.path }); }); - it('addComponent - 多个同类型组件依次添加_001,_002,...后缀', async () => { + it('add - 多个同类型组件依次添加_001,_002,...后缀', async () => { const totalCount = 5; const testComponent = 'cc.Layout'; const components: IComponentIdentifier[] = []; for (let i = 0; i < totalCount; i++) { - const comp = await ComponentProxy.addComponent({ - nodePathOrUuid: testNodePath, + const comp = await ComponentProxy.add({ + nodePath: testNodePath, component: testComponent, }); expect(comp).toBeDefined(); @@ -1300,36 +1065,109 @@ describe('Component Proxy 测试', () => { } for (const comp of components.reverse()) { - await ComponentProxy.removeComponent({ path: comp.path }); + await ComponentProxy.remove({ path: comp.path }); } }); - it('addComponent - 删除中间组件后新增应复用已删除的名称', async () => { + it('add - 删除中间组件后新增应复用已删除的名称', async () => { const testComponent = 'cc.Layout'; // 添加3个同类型组件: cc.Layout, cc.Layout_001, cc.Layout_002 - const comp0 = await ComponentProxy.addComponent({ nodePathOrUuid: testNodePath, component: testComponent }); - const comp1 = await ComponentProxy.addComponent({ nodePathOrUuid: testNodePath, component: testComponent }); - const comp2 = await ComponentProxy.addComponent({ nodePathOrUuid: testNodePath, component: testComponent }); + const comp0 = await ComponentProxy.add({ nodePath: testNodePath, component: testComponent }); + const comp1 = await ComponentProxy.add({ nodePath: testNodePath, component: testComponent }); + const comp2 = await ComponentProxy.add({ nodePath: testNodePath, component: testComponent }); expect(comp0.path).toBe(`${testNodePath}/${testComponent}`); expect(comp1.path).toBe(`${testNodePath}/${testComponent}_001`); expect(comp2.path).toBe(`${testNodePath}/${testComponent}_002`); // 删除 _001 - const removeResult = await ComponentProxy.removeComponent({ path: comp1.path }); + const removeResult = await ComponentProxy.remove({ path: comp1.path }); expect(removeResult).toBe(true); // 再添加2个,第一个应复用 _001,第二个为 _003 - const comp3 = await ComponentProxy.addComponent({ nodePathOrUuid: testNodePath, component: testComponent }); - const comp4 = await ComponentProxy.addComponent({ nodePathOrUuid: testNodePath, component: testComponent }); + const comp3 = await ComponentProxy.add({ nodePath: testNodePath, component: testComponent }); + const comp4 = await ComponentProxy.add({ nodePath: testNodePath, component: testComponent }); expect(comp3.path).toBe(`${testNodePath}/${testComponent}_001`); expect(comp4.path).toBe(`${testNodePath}/${testComponent}_003`); // 清理 - await ComponentProxy.removeComponent({ path: comp4.path }); - await ComponentProxy.removeComponent({ path: comp3.path }); - await ComponentProxy.removeComponent({ path: comp2.path }); - await ComponentProxy.removeComponent({ path: comp0.path }); + await ComponentProxy.remove({ path: comp4.path }); + await ComponentProxy.remove({ path: comp3.path }); + await ComponentProxy.remove({ path: comp2.path }); + await ComponentProxy.remove({ path: comp0.path }); + }); + }); + + describe('17. 边界情况与错误处理', () => { + it('add - 添加到不存在的节点路径应抛异常', async () => { + await expect(ComponentProxy.add({ + nodePath: 'non-existent-path', + component: 'cc.Label', + })).rejects.toThrow(); + }); + + // it('remove - 移除不存在组件路径', async () => { + // const result = await ComponentProxy.remove({ path: `${nodePath}/cc.NonExistent` }); + // expect(result).toBe(false); + // }); + + // it('query - 查询不存在组件返回 null', async () => { + // const result = await ComponentProxy.query({ path: `${nodePath}/cc.NonExistent` }); + // expect(result).toBeNull(); + // }); + + it('setProperty - 设置不存在组件路径应抛异常', async () => { + await expect(ComponentProxy.setProperty({ + componentPath: `${nodePath}/cc.NonExistent`, + properties: { string: 'test' }, + })).rejects.toThrow(); + }); + + it('setProperty - 设置不存在的属性 key 应抛异常', async () => { + const comp = await ComponentProxy.add({ nodePath, component: 'cc.Label' }); + try { + await expect(ComponentProxy.setProperty({ + componentPath: comp.path, + properties: { nonExistentProp: 42 }, + })).rejects.toThrow(); + } finally { + await ComponentProxy.remove({ path: comp.path }); + } + }); + + it('setProperty - 同时设置多个属性', async () => { + const comp = await ComponentProxy.add({ nodePath, component: 'cc.Label' }); + try { + const result = await ComponentProxy.setProperty({ + componentPath: comp.path, + properties: { fontSize: 60, string: 'multi-test' }, + }); + expect(result).toBe(true); + + const updated = await ComponentProxy.query({ path: comp.path }) as IComponentInfo; + expect(updated.properties['fontSize'].value).toBe(60); + expect(updated.properties['string'].value).toBe('multi-test'); + } finally { + await ComponentProxy.remove({ path: comp.path }); + } + }); + }); + + describe('18. queryAll - 查询所有组件类型', () => { + it('queryAll - 返回字符串数组', async () => { + const result = await ComponentProxy.queryAll(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + for (const item of result) { + expect(typeof item).toBe('string'); + } + }); + + it('queryAll - 包含已知内置组件', async () => { + const result = await ComponentProxy.queryAll(); + expect(result).toContain('cc.Label'); + expect(result).toContain('cc.Sprite'); + expect(result).toContain('cc.Button'); }); }); }); \ No newline at end of file diff --git a/src/core/scene/test/editor-proxy-prefab.testcase.ts b/src/core/scene/test/editor-proxy-prefab.testcase.ts index 609dc9234..4a710bed2 100644 --- a/src/core/scene/test/editor-proxy-prefab.testcase.ts +++ b/src/core/scene/test/editor-proxy-prefab.testcase.ts @@ -1,4 +1,4 @@ -import { IBaseIdentifier, INode, NodeType, ReloadResult, TEditorEntity, } from '../common'; +import { IBaseIdentifier, INodeInfo, NodeType, ReloadResult } from '../common'; import { EditorProxy } from '../main-process/proxy/editor-proxy'; import { SceneTestEnv } from './scene-test-env'; import { NodeProxy } from '../main-process/proxy/node-proxy'; @@ -10,7 +10,7 @@ describe('EditorProxy Prefab 测试', () => { describe('预制体操作', () => { let identifier: IBaseIdentifier | null = null; let instanceAssetURL = ''; - let entity: TEditorEntity | null = null; + let entity: INodeInfo | null = null; it('create - 创建新预制体', async () => { identifier = await EditorProxy.create({ @@ -30,7 +30,7 @@ describe('EditorProxy Prefab 测试', () => { expect(instanceAssetURL).toBeTruthy(); expect(identifier).toBeTruthy(); - const result = await EditorProxy.open({ urlOrUUID: instanceAssetURL }) as INode; + const result = await EditorProxy.open({ urlOrUUID: instanceAssetURL }) as INodeInfo; expect(result).toBeTruthy(); expect(result?.prefab).toBeTruthy(); @@ -41,7 +41,7 @@ describe('EditorProxy Prefab 测试', () => { it('save - 通过 UUID 保存预制体', async () => { expect(identifier).toBeTruthy(); - await NodeProxy.createNodeByType({ + await NodeProxy.createByType({ path: '', nodeType: NodeType.EMPTY, name: 'prefab-test-node-uuid', @@ -95,7 +95,7 @@ describe('EditorProxy Prefab 测试', () => { expect(instanceAssetURL).toBeTruthy(); - entity = await EditorProxy.open({ urlOrUUID: instanceAssetURL }) as INode; + entity = await EditorProxy.open({ urlOrUUID: instanceAssetURL }) as INodeInfo; expect(entity).toBeTruthy(); expect(entity?.prefab).toBeTruthy(); @@ -109,7 +109,7 @@ describe('EditorProxy Prefab 测试', () => { it('save - 通过 URL 保存预制体', async () => { expect(instanceAssetURL).toBeTruthy(); - await NodeProxy.createNodeByType({ + await NodeProxy.createByType({ path: '', nodeType: NodeType.EMPTY, name: 'prefab-test-node-url', @@ -164,7 +164,7 @@ describe('EditorProxy Prefab 测试', () => { urlOrUUID: SceneTestEnv.prefabURL, }); - const node = await NodeProxy.createNodeByType({ + const node = await NodeProxy.createByType({ path: '', nodeType: NodeType.EMPTY, name: 'current-prefab-test-node', @@ -172,8 +172,8 @@ describe('EditorProxy Prefab 测试', () => { expect(node).not.toBeNull(); - const label = await ComponentProxy.addComponent({ - nodePathOrUuid: node?.path as string, + const label = await ComponentProxy.add({ + nodePath: node?.path as string, component: 'cc.Label' }); await ComponentProxy.setProperty({ diff --git a/src/core/scene/test/editor-proxy-scene.testcase.ts b/src/core/scene/test/editor-proxy-scene.testcase.ts index 8723696b5..ef45e9223 100644 --- a/src/core/scene/test/editor-proxy-scene.testcase.ts +++ b/src/core/scene/test/editor-proxy-scene.testcase.ts @@ -1,4 +1,4 @@ -import { IBaseIdentifier, IScene, NodeType, TEditorEntity, ReloadResult } from '../common'; +import { IBaseIdentifier, INodeInfo, ISceneInfo, NodeType, ReloadResult } from '../common'; import { EditorProxy } from '../main-process/proxy/editor-proxy'; import { SceneTestEnv } from './scene-test-env'; import { NodeProxy } from '../main-process/proxy/node-proxy'; @@ -8,7 +8,7 @@ import { assetManager } from '../../assets'; describe('EditorProxy Scene 测试', () => { describe('场景操作', () => { let identifier: IBaseIdentifier | null = null; - let entity: TEditorEntity | null = null; + let entity: ISceneInfo | INodeInfo | null = null; it('create - 创建新场景', async () => { identifier = await EditorProxy.create({ @@ -26,7 +26,7 @@ describe('EditorProxy Scene 测试', () => { const result = await EditorProxy.open({ urlOrUUID: identifier.assetUuid - }) as IScene; + }) as ISceneInfo; expect(result).toBeDefined(); expect(result.assetUuid).toBe(identifier.assetUuid); }); @@ -35,7 +35,7 @@ describe('EditorProxy Scene 测试', () => { expect(identifier).toBeTruthy(); if (!identifier) return; - await NodeProxy.createNodeByType({ + await NodeProxy.createByType({ path: '', nodeType: NodeType.EMPTY, name: 'scene-test-node-uuid', @@ -85,7 +85,7 @@ describe('EditorProxy Scene 测试', () => { entity = await EditorProxy.open({ urlOrUUID: identifier.assetUrl - }) as IScene; + }) as ISceneInfo; expect(entity).toBeDefined(); expect(entity.assetUrl).toBe(identifier.assetUrl); }); @@ -94,7 +94,7 @@ describe('EditorProxy Scene 测试', () => { await EditorProxy.open({ urlOrUUID: SceneTestEnv.sceneURL, }); - await NodeProxy.createNodeByType({ + await NodeProxy.createByType({ path: '', nodeType: NodeType.EMPTY, name: 'scene-test-node-url', @@ -139,7 +139,7 @@ describe('EditorProxy Scene 测试', () => { await EditorProxy.open({ urlOrUUID: SceneTestEnv.sceneURL, }); - await NodeProxy.createNodeByType({ + await NodeProxy.createByType({ path: '', nodeType: NodeType.EMPTY, name: 'current-scene-test-node', @@ -192,7 +192,7 @@ describe('EditorProxy Scene 测试', () => { const result = await EditorProxy.open({ urlOrUUID: identifierA.assetUuid - }) as IScene; + }) as ISceneInfo; expect(result).toBeDefined(); expect(result.assetUuid).toBe(identifierA.assetUuid); }); @@ -223,7 +223,7 @@ describe('EditorProxy Scene 测试', () => { const result = await EditorProxy.open({ urlOrUUID: identifierB.assetUuid - }) as IScene; + }) as ISceneInfo; expect(result).toBeDefined(); expect(result.assetUuid).toBe(identifierB.assetUuid); }); diff --git a/src/core/scene/test/engine-proxy.testcase.ts b/src/core/scene/test/engine-proxy.testcase.ts index be4523dd9..57b2704f5 100644 --- a/src/core/scene/test/engine-proxy.testcase.ts +++ b/src/core/scene/test/engine-proxy.testcase.ts @@ -1,4 +1,4 @@ -import { IEngineEvents, INode, NodeType, } from '../common'; +import { IEngineEvents, INodeInfo, NodeType, } from '../common'; import * as utils from './utils'; @@ -30,7 +30,7 @@ describe('Engine Proxy 测试', () => { const eventSceneUpdatePromise = utils.once(sceneWorker, 'engine:update'); const eventSceneTickedPromise = utils.once(sceneWorker, 'engine:ticked'); - const createdNode = await NodeProxy.createNodeByType({ + const createdNode = await NodeProxy.createByType({ path: '', name: 'TestNode', nodeType: NodeType.EMPTY, @@ -46,7 +46,7 @@ describe('Engine Proxy 测试', () => { const eventSceneUpdatePromise = utils.once(sceneWorker, 'engine:update'); const eventSceneTickedPromise = utils.once(sceneWorker, 'engine:ticked'); - await NodeProxy.updateNode({ + await NodeProxy.update({ path: nodePath, properties: { position: { x: 5, y: 5, z: 5 } @@ -62,8 +62,8 @@ describe('Engine Proxy 测试', () => { const eventSceneUpdatePromise = utils.once(sceneWorker, 'engine:update'); const eventSceneTickedPromise = utils.once(sceneWorker, 'engine:ticked'); - const component = await ComponentProxy.addComponent({ - nodePathOrUuid: nodePath, + const component = await ComponentProxy.add({ + nodePath: nodePath, component: 'cc.Label' }); componentPath = component.path; @@ -93,7 +93,7 @@ describe('Engine Proxy 测试', () => { const eventSceneUpdatePromise = utils.once(sceneWorker, 'engine:update'); const eventSceneTickedPromise = utils.once(sceneWorker, 'engine:ticked'); - await ComponentProxy.removeComponent({ path: componentPath }); + await ComponentProxy.remove({ path: componentPath }); await eventSceneUpdatePromise; await eventSceneTickedPromise; @@ -104,7 +104,7 @@ describe('Engine Proxy 测试', () => { const eventSceneUpdatePromise = utils.once(sceneWorker, 'engine:update'); const eventSceneTickedPromise = utils.once(sceneWorker, 'engine:ticked'); - await NodeProxy.deleteNode({ + await NodeProxy.delete({ path: nodePath, keepWorldTransform: false }); diff --git a/src/core/scene/test/node-for-editor.testcase.ts b/src/core/scene/test/node-for-editor.testcase.ts new file mode 100644 index 000000000..13ca87bd6 --- /dev/null +++ b/src/core/scene/test/node-for-editor.testcase.ts @@ -0,0 +1,724 @@ +/* + * TODO(qgh):理论上不应该放在这里测试,因为这些接口不是通过proxy调用的 + 而是直接通过service调用的。因为目前还未实现service接口的直接测试,因此是简单的实现。 + 后续需要迁移 + */ +import { + type ICreateByNodeTypeParams, + type IQueryNodeParams, + type INodeInfo, + type INode, + type ISetPropertyOptions, + NodeType, +} from '../common'; +import { type IScene } from '../common/editor/scene'; +import { NodeProxy } from '../main-process/proxy/node-proxy'; +import { EditorProxy } from '../main-process/proxy/editor-proxy'; +import { Rpc } from '../main-process/rpc'; +import { SceneTestEnv } from './scene-test-env'; + +// 这些接口未在 IPublicNodeService 中暴露,测试中直接通过 RPC 调用 +const rpcRequest = (method: string, args?: any[]) => + (Rpc.getInstance() as any).request('Node', method, args); + +function queryNodeDump(path: string): Promise { + return rpcRequest('query', [{ path, queryChildren: false, queryComponent: false }]); +} + +function setNodeProperty(options: ISetPropertyOptions): Promise { + return rpcRequest('setProperty', [options]); +} + +function previewSetNodeProperty(options: ISetPropertyOptions): Promise { + return rpcRequest('previewSetProperty', [options]); +} + +function cancelPreviewSetNodeProperty(options: ISetPropertyOptions): Promise { + return rpcRequest('cancelPreviewSetProperty', [options]); +} + +function resetNode(path: string): Promise { + return rpcRequest('reset', [path]); +} + +function resetNodeProperty(options: ISetPropertyOptions): Promise { + return rpcRequest('resetProperty', [options]); +} + +function updateNodePropertyFromNull(options: ISetPropertyOptions): Promise { + return rpcRequest('updatePropertyFromNull', [options]); +} + +function setNodeAndChildrenLayer(options: ISetPropertyOptions): Promise { + return rpcRequest('setNodeAndChildrenLayer', [options]); +} + +describe('Node ForEditor 接口测试', () => { + let testNode: INodeInfo | null = null; + let testNodeUuid = ''; + const testNodeName = 'DumpTestNode'; + + beforeAll(async () => { + await EditorProxy.open({ + urlOrUUID: SceneTestEnv.sceneURL, + }); + const params: ICreateByNodeTypeParams = { + path: '/', + name: testNodeName, + nodeType: NodeType.EMPTY, + }; + testNode = await NodeProxy.createByType(params); + expect(testNode).toBeDefined(); + + // 通过 queryNode 获取节点 UUID + const queryParams: IQueryNodeParams = { + path: testNode!.path, + queryChildren: false, + queryComponent: false, + }; + const nodeInfo = await NodeProxy.query(queryParams) as INodeInfo | null; + expect(nodeInfo).not.toBeNull(); + testNodeUuid = nodeInfo!.nodeId; + }); + + afterAll(async () => { + if (testNode) { + await NodeProxy.delete({ path: testNode.path, keepWorldTransform: false }); + } + await EditorProxy.close({ + urlOrUUID: SceneTestEnv.sceneURL, + }); + }); + + describe('1. query - 查询节点 dump 数据', () => { + it('query - 传入 path 返回 INode', async () => { + const result = await rpcRequest('query', [{ path: testNode!.path, queryChildren: false, queryComponent: false }]) as INode | null; + expect(result).not.toBeNull(); + expect(result!.name).toBeDefined(); + expect(result!.name.value).toBe(testNodeName); + expect(result!.active).toBeDefined(); + expect(result!.position).toBeDefined(); + expect(result!.rotation).toBeDefined(); + expect(result!.scale).toBeDefined(); + expect(result!.layer).toBeDefined(); + expect(result!.uuid).toBeDefined(); + expect(result!.__comps__).toBeDefined(); + expect(result!.__type__).toBeDefined(); + }); + + it('query - 查询有效节点返回 dump 数据', async () => { + const dump = await queryNodeDump(testNode!.path); + expect(dump).not.toBeNull(); + expect(dump).toBeDefined(); + }); + + it('query - dump 包含必要字段', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + expect(dump).not.toBeNull(); + + // 基本属性字段 + expect(dump.name).toBeDefined(); + expect(dump.name.value).toBe(testNodeName); + expect(dump.active).toBeDefined(); + expect(dump.active.value).toBe(true); + expect(dump.position).toBeDefined(); + expect(dump.rotation).toBeDefined(); + expect(dump.scale).toBeDefined(); + expect(dump.layer).toBeDefined(); + expect(dump.uuid).toBeDefined(); + + // 结构字段 + expect(dump.__comps__).toBeDefined(); + expect(Array.isArray(dump.__comps__)).toBe(true); + expect(dump.__type__).toBeDefined(); + }); + + it('query - 查询不存在的节点返回 null', async () => { + const dump = await queryNodeDump('non-existent-path'); + expect(dump).toBeNull(); + }); + + it('query - 不传参数返回根节点 dump 数据', async () => { + const dump = await rpcRequest('query', []); + expect(dump).not.toBeNull(); + const sceneResult = dump as IScene; + expect(sceneResult.isScene).toBeTruthy(); + expect(sceneResult.__type__).toBeDefined(); + expect(sceneResult.uuid).toBeDefined(); + expect(Array.isArray(sceneResult.children)).toBe(true); + }); + + it('query - 传入 "/" 返回场景根节点', async () => { + const dump = await rpcRequest('query', [{ path: '/', queryChildren: false, queryComponent: false }]); + expect(dump).not.toBeNull(); + const sceneResult = dump as IScene; + expect(sceneResult.isScene).toBeTruthy(); + expect(sceneResult.__type__).toBeDefined(); + expect(sceneResult.uuid).toBeDefined(); + expect(Array.isArray(sceneResult.children)).toBe(true); + }); + }); + + describe('2. setProperty - 设置节点属性', () => { + it('setProperty - 修改节点位置', async () => { + // 先获取当前 dump 作为模板 + const dump = await queryNodeDump(testNode!.path) as INode; + const positionDump = { ...dump.position, value: { x: 100, y: 200, z: 0 } }; + + const options: ISetPropertyOptions = { + nodePath: testNode!.path, + path: 'position', + dump: positionDump, + }; + const result = await setNodeProperty(options); + expect(result).toBe(true); + + // 验证修改生效 + const updatedDump = await queryNodeDump(testNode!.path) as INode; + expect(updatedDump.position.value).toEqual({ x: 100, y: 200, z: 0 }); + }); + + it('setProperty - 修改节点名称', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + const nameDump = { ...dump.name, value: 'RenamedNode' }; + + const result = await setNodeProperty({ + nodePath: testNode!.path, + path: 'name', + dump: nameDump, + }); + expect(result).toBe(true); + + const updatedDump = await queryNodeDump(testNode!.path) as INode; + expect(updatedDump.name.value).toBe('RenamedNode'); + + // 还原名称 + const restoreDump = { ...updatedDump.name, value: testNodeName }; + await setNodeProperty({ + nodePath: testNode!.path, + path: 'name', + dump: restoreDump, + }); + }); + + it('setProperty - 修改节点 active 状态', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + const activeDump = { ...dump.active, value: false }; + + const result = await setNodeProperty({ + nodePath: testNode!.path, + path: 'active', + dump: activeDump, + }); + expect(result).toBe(true); + + const updatedDump = await queryNodeDump(testNode!.path) as INode; + expect(updatedDump.active.value).toBe(false); + + // 还原 + await setNodeProperty({ + nodePath: testNode!.path, + path: 'active', + dump: { ...updatedDump.active, value: true }, + }); + }); + + it('setProperty - 修改节点缩放', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + const scaleDump = { ...dump.scale, value: { x: 2, y: 2, z: 2 } }; + + const result = await setNodeProperty({ + nodePath: testNode!.path, + path: 'scale', + dump: scaleDump, + }); + expect(result).toBe(true); + + const updatedDump = await queryNodeDump(testNode!.path) as INode; + expect(updatedDump.scale.value).toEqual({ x: 2, y: 2, z: 2 }); + + // 还原 + await setNodeProperty({ + nodePath: testNode!.path, + path: 'scale', + dump: { ...updatedDump.scale, value: { x: 1, y: 1, z: 1 } }, + }); + }); + + it('setProperty - 不存在的节点返回 false', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + const result = await setNodeProperty({ + nodePath: 'non-existent-path', + path: 'position', + dump: dump.position, + }); + expect(result).toBe(false); + }); + }); + + describe('3. previewSetProperty / cancelPreviewSetProperty - 预览与取消', () => { + let labelNodeUuid = ''; + let labelNode: INodeInfo | null = null; + + beforeAll(async () => { + // 创建 Label 节点,自带组件,适合测试多层路径的预览 + labelNode = await NodeProxy.createByType({ + path: '/', + name: 'PreviewTestLabel', + nodeType: NodeType.LABEL, + }); + expect(labelNode).toBeDefined(); + + const nodeInfo = await NodeProxy.query({ + path: labelNode!.path, + queryChildren: false, + queryComponent: false, + }) as INodeInfo | null; + labelNodeUuid = nodeInfo!.nodeId; + }); + + afterAll(async () => { + if (labelNode) { + await NodeProxy.delete({ path: labelNode.path, keepWorldTransform: false }); + } + }); + + it('预览修改组件属性后取消,值应恢复', async () => { + // 获取原始 dump,找到 Label 组件的 string 属性 + const originalDump = await queryNodeDump(labelNode!.path) as INode; + expect(originalDump.__comps__.length).toBeGreaterThan(0); + + // 找到 cc.Label 组件的索引(通常在 UITransform 之后) + let labelCompIndex = -1; + for (let i = 0; i < originalDump.__comps__.length; i++) { + const comp = originalDump.__comps__[i]; + if (comp.type === 'cc.Label') { + labelCompIndex = i; + break; + } + } + expect(labelCompIndex).toBeGreaterThanOrEqual(0); + + const labelComp = originalDump.__comps__[labelCompIndex]; + const compValue = labelComp.value as Record; + const originalString = compValue['string'].value; + const stringDump = { ...compValue['string'], value: 'preview-test-value' }; + const previewPath = `__comps__.${labelCompIndex}.string`; + + // 预览修改 + const previewResult = await previewSetNodeProperty({ + nodePath: labelNode!.path, + path: previewPath, + dump: stringDump, + }); + expect(previewResult).toBe(true); + + // 验证预览已生效 + const previewedDump = await queryNodeDump(labelNode!.path) as INode; + const previewedComp = previewedDump.__comps__[labelCompIndex].value as Record; + expect(previewedComp['string'].value).toBe('preview-test-value'); + + // 取消预览 + const cancelResult = await cancelPreviewSetNodeProperty({ + nodePath: labelNode!.path, + path: previewPath, + dump: stringDump, + }); + expect(cancelResult).toBe(true); + + // 验证已恢复原值 + const restoredDump = await queryNodeDump(labelNode!.path) as INode; + const restoredComp = restoredDump.__comps__[labelCompIndex].value as Record; + expect(restoredComp['string'].value).toBe(originalString); + }); + + it('预览修改后正式提交,值应保留', async () => { + const originalDump = await queryNodeDump(labelNode!.path) as INode; + + let labelCompIndex = -1; + for (let i = 0; i < originalDump.__comps__.length; i++) { + if (originalDump.__comps__[i].type === 'cc.Label') { + labelCompIndex = i; + break; + } + } + expect(labelCompIndex).toBeGreaterThanOrEqual(0); + + const compValue = originalDump.__comps__[labelCompIndex].value as Record; + const stringDump = { ...compValue['string'], value: 'committed-value' }; + const previewPath = `__comps__.${labelCompIndex}.string`; + + // 预览修改 + await previewSetNodeProperty({ + nodePath: labelNode!.path, + path: previewPath, + dump: stringDump, + }); + + // 正式提交相同的值 + const commitResult = await setNodeProperty({ + nodePath: labelNode!.path, + path: previewPath, + dump: stringDump, + }); + expect(commitResult).toBe(true); + + // 验证值已保留 + const committedDump = await queryNodeDump(labelNode!.path) as INode; + const committedComp = committedDump.__comps__[labelCompIndex].value as Record; + expect(committedComp['string'].value).toBe('committed-value'); + }); + }); + + describe('4. reset - 重置节点变换', () => { + it('reset - 修改后重置,变换属性恢复默认', async () => { + // 先修改位置和缩放 + const dump = await queryNodeDump(testNode!.path) as INode; + await setNodeProperty({ + nodePath: testNode!.path, + path: 'position', + dump: { ...dump.position, value: { x: 100, y: 200, z: 300 } }, + }); + await setNodeProperty({ + nodePath: testNode!.path, + path: 'scale', + dump: { ...dump.scale, value: { x: 5, y: 5, z: 5 } }, + }); + + // 重置节点 + const result = await resetNode(testNode!.path); + expect(result).toBe(true); + + // 验证变换属性恢复默认 + const resetDump = await queryNodeDump(testNode!.path) as INode; + expect(resetDump.position.value).toEqual({ x: 0, y: 0, z: 0 }); + expect(resetDump.scale.value).toEqual({ x: 1, y: 1, z: 1 }); + }); + }); + + describe('5. resetProperty - 重置单个属性', () => { + it('resetProperty - 重置位置属性', async () => { + // 先修改位置 + const dump = await queryNodeDump(testNode!.path) as INode; + await setNodeProperty({ + nodePath: testNode!.path, + path: 'position', + dump: { ...dump.position, value: { x: 42, y: 42, z: 42 } }, + }); + + // 重置 position + const result = await resetNodeProperty({ + nodePath: testNode!.path, + path: 'position', + dump: dump.position, + }); + expect(result).toBe(true); + + const resetDump = await queryNodeDump(testNode!.path) as INode; + expect(resetDump.position.value).toEqual({ x: 0, y: 0, z: 0 }); + }); + + it('resetProperty - 重置缩放属性', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + await setNodeProperty({ + nodePath: testNode!.path, + path: 'scale', + dump: { ...dump.scale, value: { x: 3, y: 3, z: 3 } }, + }); + + const result = await resetNodeProperty({ + nodePath: testNode!.path, + path: 'scale', + dump: dump.scale, + }); + expect(result).toBe(true); + + const resetDump = await queryNodeDump(testNode!.path) as INode; + expect(resetDump.scale.value).toEqual({ x: 1, y: 1, z: 1 }); + }); + }); + + describe('6. setNodeAndChildrenLayer - 递归设置 layer', () => { + let parentNode: INodeInfo | null = null; + let childNode: INodeInfo | null = null; + let parentUuid = ''; + let childUuid = ''; + + beforeAll(async () => { + // 创建父节点 + parentNode = await NodeProxy.createByType({ + path: '/', + name: 'LayerParent', + nodeType: NodeType.EMPTY, + }); + expect(parentNode).toBeDefined(); + + // 创建子节点 + childNode = await NodeProxy.createByType({ + path: parentNode!.path, + name: 'LayerChild', + nodeType: NodeType.EMPTY, + }); + expect(childNode).toBeDefined(); + + // 获取 UUID + const parentInfo = await NodeProxy.query({ + path: parentNode!.path, + queryChildren: false, + queryComponent: false, + }) as INodeInfo | null; + parentUuid = parentInfo!.nodeId; + + const childInfo = await NodeProxy.query({ + path: childNode!.path, + queryChildren: false, + queryComponent: false, + }) as INodeInfo | null; + childUuid = childInfo!.nodeId; + }); + + afterAll(async () => { + if (parentNode) { + await NodeProxy.delete({ path: parentNode.path, keepWorldTransform: false }); + } + }); + + it('setNodeAndChildrenLayer - 父子节点 layer 统一设置', async () => { + const dump = await queryNodeDump(parentNode!.path) as INode; + const targetLayer = 1 << 25; // UI_2D layer + const layerDump = { ...dump.layer, value: targetLayer }; + + await setNodeAndChildrenLayer({ + nodePath: parentNode!.path, + path: 'layer', + dump: layerDump, + }); + + // 验证父节点 + const parentDump = await queryNodeDump(parentNode!.path) as INode; + expect(parentDump.layer.value).toBe(targetLayer); + + // 验证子节点 + const childDump = await queryNodeDump(childNode!.path) as INode; + expect(childDump.layer.value).toBe(targetLayer); + }); + }); + + describe('7. updatePropertyFromNull - 初始化 null 属性', () => { + it('updatePropertyFromNull - 调用不报错', async () => { + // 该接口用于将 null 类型属性初始化为可编辑值 + // 对于 Empty 节点的基本属性(position 等),不存在 null 情况 + // 这里验证接口调用不抛异常即可 + const dump = await queryNodeDump(testNode!.path) as INode; + const result = await updateNodePropertyFromNull({ + nodePath: testNode!.path, + path: 'position', + dump: dump.position, + }); + expect(typeof result).toBe('boolean'); + }); + }); + + describe('8. IProperty - encodeObject 编码字段验证', () => { + it('position 属性包含 ForEditor 特有字段', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + const position = dump.position; + + // ForEditor 编码特有字段 + expect(position.type).toBeDefined(); + expect(position.visible).toBe(true); + expect(typeof position.readonly).toBe('boolean'); + expect(typeof position.animatable).toBe('boolean'); + expect(position.default).toBeDefined(); + expect(position.displayName).toBeDefined(); + }); + + it('position 的 default 值是 Vec3 结构', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + const position = dump.position; + + // default 应该被递归编码为嵌套的 IProperty 结构 + expect(position.default).toBeDefined(); + if (typeof position.default === 'object' && position.default !== null) { + expect(position.default.type).toBeDefined(); + expect(position.default.value).toBeDefined(); + } + }); + + it('position 的子属性 (x, y, z) 也是 IProperty 结构', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + const positionValue = dump.position.value as Record; + + expect(positionValue).toBeDefined(); + // Vec3 的子属性应该有 value 字段 + if (positionValue.x && typeof positionValue.x === 'object') { + expect(positionValue.x.value).toBeDefined(); + expect(typeof positionValue.x.value).toBe('number'); + } + }); + + it('name 属性 animatable 为 false', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + expect(dump.name.animatable).toBe(false); + }); + + it('uuid 属性 animatable 为 false', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + expect(dump.uuid.animatable).toBe(false); + }); + + it('layer 属性是 Enum 类型,带 enumList', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + const layer = dump.layer; + + expect(layer.type).toBe('Enum'); + expect(layer.enumList).toBeDefined(); + expect(Array.isArray(layer.enumList)).toBe(true); + expect(layer.enumList!.length).toBeGreaterThan(0); + // enumList 的每项有 name 和 value + const firstEnum = layer.enumList![0]; + expect(firstEnum.name).toBeDefined(); + expect(firstEnum.value).toBeDefined(); + }); + + it('mobility 属性是 Enum 类型,带 enumList', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + const mobility = dump.mobility; + + expect(mobility.type).toBe('Enum'); + expect(mobility.enumList).toBeDefined(); + expect(Array.isArray(mobility.enumList)).toBe(true); + expect(mobility.enumList!.length).toBeGreaterThan(0); + }); + + it('active 属性的 value 是布尔类型', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + const active = dump.active; + + expect(typeof active.value).toBe('boolean'); + expect(active.visible).toBe(true); + expect(active.displayName).toBe('Active'); + }); + + it('children 是 IProperty 数组', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + + expect(Array.isArray(dump.children)).toBe(true); + // 空节点可能没有子节点,验证结构不出错即可 + }); + + it('__comps__ 中的组件 dump 包含 editor 附加信息', async () => { + // 创建一个带组件的节点来测试 + const labelNode = await NodeProxy.createByType({ + path: '/', + name: 'PropertyTestLabel', + nodeType: NodeType.LABEL, + }); + expect(labelNode).toBeDefined(); + + const dump = await queryNodeDump(labelNode!.path) as INode; + expect(dump.__comps__.length).toBeGreaterThan(0); + + // 找到 cc.Label 组件 + const labelComp = dump.__comps__.find(c => c.type === 'cc.Label'); + expect(labelComp).toBeDefined(); + + // 组件 dump 应包含 editor 附加数据 + if (labelComp?.editor) { + expect(typeof labelComp.editor).toBe('object'); + } + + // 组件 dump 的 value 中的属性也应是 IProperty 结构 + const compValue = labelComp!.value as Record; + expect(compValue['string']).toBeDefined(); + expect(compValue['string'].value).toBeDefined(); + expect(compValue['string'].type).toBeDefined(); + + // 组件应有 extends 继承链 + if (labelComp?.extends) { + expect(Array.isArray(labelComp.extends)).toBe(true); + expect(labelComp.extends.length).toBeGreaterThan(0); + } + + await NodeProxy.delete({ path: labelNode!.path, keepWorldTransform: false }); + }); + }); + + describe('9. dump path 属性填充验证', () => { + it('节点 dump 的 path 字段是节点树路径', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + expect(typeof dump.path).toBe('string'); + expect(dump.path.length).toBeGreaterThan(0); + expect(dump.path).toContain(testNodeName); + }); + + it('场景根节点 dump 的 path 为 "/"', async () => { + const dump = await rpcRequest('query', []) as IScene; + expect(dump.path).toBe('/'); + }); + + it('节点顶层 IProperty 字段的 path 等于其 key 名', async () => { + const dump = await queryNodeDump(testNode!.path) as INode; + + expect(dump.active.path).toBe('active'); + expect(dump.locked.path).toBe('locked'); + expect(dump.name.path).toBe('name'); + expect(dump.position.path).toBe('position'); + expect(dump.rotation.path).toBe('rotation'); + expect(dump.scale.path).toBe('scale'); + expect(dump.mobility.path).toBe('mobility'); + expect(dump.layer.path).toBe('layer'); + expect(dump.uuid.path).toBe('uuid'); + }); + + it('组件属性的 path 格式为 __comps__.{index}.{key}', async () => { + const labelNode = await NodeProxy.createByType({ + path: '/', + name: 'PathTestLabel', + nodeType: NodeType.LABEL, + }); + expect(labelNode).toBeDefined(); + + const dump = await queryNodeDump(labelNode!.path) as INode; + expect(dump.__comps__.length).toBeGreaterThan(0); + + let labelCompIndex = -1; + for (let i = 0; i < dump.__comps__.length; i++) { + if (dump.__comps__[i].type === 'cc.Label') { + labelCompIndex = i; + break; + } + } + expect(labelCompIndex).toBeGreaterThanOrEqual(0); + + const compValue = dump.__comps__[labelCompIndex].value as Record; + expect(compValue['string']).toBeDefined(); + expect(compValue['string'].path).toBe(`__comps__.${labelCompIndex}.string`); + + expect(compValue['enabled']).toBeDefined(); + expect(compValue['enabled'].path).toBe(`__comps__.${labelCompIndex}.enabled`); + + await NodeProxy.delete({ path: labelNode!.path, keepWorldTransform: false }); + }); + + it('场景顶层 IProperty 字段的 path 等于其 key 名', async () => { + const dump = await rpcRequest('query', []) as IScene; + + expect(dump.active.path).toBe('active'); + expect(dump.name.path).toBe('name'); + expect(dump.uuid.path).toBe('uuid'); + }); + + it('场景 _globals 属性的 path 格式为 _globals.{key}', async () => { + const dump = await rpcRequest('query', []) as IScene; + + if (dump._globals && typeof dump._globals === 'object') { + for (const [key, val] of Object.entries(dump._globals)) { + if (val && typeof val === 'object' && 'type' in val && 'value' in val) { + expect((val as any).path).toBe(`_globals.${key}`); + } + } + } + }); + }); +}); diff --git a/src/core/scene/test/node-proxy.testcase.ts b/src/core/scene/test/node-proxy.testcase.ts index adc2081de..ab7f437bb 100644 --- a/src/core/scene/test/node-proxy.testcase.ts +++ b/src/core/scene/test/node-proxy.testcase.ts @@ -5,8 +5,9 @@ import { type IQueryNodeParams, type IQueryNodeTreeParams, type IUpdateNodeParams, - type INode, + type INodeInfo, NodeType, + MobilityMode, } from '../common'; import { IVec3 } from '../common/value-types'; import { NodeProxy } from '../main-process/proxy/node-proxy'; @@ -14,7 +15,7 @@ import { SceneTestEnv } from './scene-test-env'; import { EditorProxy } from '../main-process/proxy/editor-proxy'; describe('Node Proxy 测试', () => { - let createdNode: INode | null = null; + let createdNode: INodeInfo | null = null; const testNodePath = '/TestNode'; const testPosition: IVec3 = { x: 1, y: 2, z: 0 }; @@ -31,7 +32,7 @@ describe('Node Proxy 测试', () => { }); describe('1. 基础节点操作', () => { - it('createNode - 创建多级父节点的节点', async () => { + it('createByType - 创建多级父节点的节点', async () => { const multiParentPath = 'Canvas/TestNode/TestNode2/TestNode3'; const params: ICreateByNodeTypeParams = { path: multiParentPath, @@ -40,14 +41,14 @@ describe('Node Proxy 测试', () => { position: testPosition }; - createdNode = await NodeProxy.createNodeByType(params); + createdNode = await NodeProxy.createByType(params); expect(createdNode).toBeDefined(); expect(createdNode?.name).toBe('TestNode'); expect(createdNode?.path).toBe(multiParentPath + '/TestNode'); }); - it('createNode - 创建带预制体的节点', async () => { + it('createByAsset - 创建带预制体的节点', async () => { const params: ICreateByAssetParams = { dbURL: 'db://internal/default_prefab/ui/Label.prefab', @@ -55,13 +56,13 @@ describe('Node Proxy 测试', () => { name: 'PrefabNode', }; - const prefabNode = await NodeProxy.createNodeByAsset(params); + const prefabNode = await NodeProxy.createByAsset(params); expect(prefabNode).toBeDefined(); expect(prefabNode?.name).toBe('PrefabNode'); console.log('Created prefab node path=', prefabNode?.path); }); - it('createNode - 创建新节点', async () => { + it('createByType - 创建新节点', async () => { const params: ICreateByNodeTypeParams = { path: testNodePath, name: 'TestNode', @@ -69,7 +70,7 @@ describe('Node Proxy 测试', () => { position: testPosition }; - createdNode = await NodeProxy.createNodeByType(params); + createdNode = await NodeProxy.createByType(params); expect(createdNode).toBeDefined(); expect(createdNode?.name).toBe('TestNode'); // 会在根节点下先创建 TestNode 再创建 Canvas/TestNode (SPRITE 节点会在 Canvas 下创建, 节点重名为 ‘TestNode’) @@ -80,7 +81,7 @@ describe('Node Proxy 测试', () => { }); describe('2. 节点查询操作(依赖创建的节点)', () => { - it('queryNode - 查询节点基本信息', async () => { + it('query - 查询节点基本信息', async () => { expect(createdNode).not.toBeNull(); if (createdNode) { const params: IQueryNodeParams = { @@ -89,14 +90,14 @@ describe('Node Proxy 测试', () => { queryComponent: true }; - const result = await NodeProxy.queryNode(params); + const result = await NodeProxy.query(params) as INodeInfo | null; expect(result).toBeDefined(); expect(result?.path).toBe('TestNode/Canvas/TestNode'); expect(result?.name).toBe('TestNode'); } }); - it('queryNode - 查询节点及子节点信息', async () => { + it('query - 查询节点及子节点信息', async () => { expect(createdNode).not.toBeNull(); if (createdNode) { const params: IQueryNodeParams = { @@ -105,14 +106,62 @@ describe('Node Proxy 测试', () => { queryComponent: false }; - const result = await NodeProxy.queryNode(params); + const result = await NodeProxy.query(params) as INodeInfo | null; expect(result).toBeDefined(); } }); + + it('query - 不传参数返回场景根节点 INodeInfo', async () => { + const result = await NodeProxy.query(); + expect(result).not.toBeNull(); + const node = result as INodeInfo; + expect(node.nodeId).toBeDefined(); + expect(node.path).toBe('/'); + expect(node.properties).toBeDefined(); + expect(node.children).toBeDefined(); + expect(Array.isArray(node.children)).toBe(true); + }); + + it('query - 传入 "/" 返回场景根节点 INodeInfo', async () => { + const result = await NodeProxy.query({ path: '/', queryChildren: false, queryComponent: false }); + expect(result).not.toBeNull(); + const node = result as INodeInfo; + expect(node.nodeId).toBeDefined(); + expect(node.path).toBe('/'); + expect(node.properties).toBeDefined(); + }); + + it('query - queryComponent:true 返回组件详细信息', async () => { + expect(createdNode).not.toBeNull(); + if (createdNode) { + const result = await NodeProxy.query({ + path: createdNode.path, + queryChildren: false, + queryComponent: true, + }) as INodeInfo | null; + expect(result).toBeDefined(); + expect(result?.components).toBeDefined(); + expect(Array.isArray(result?.components)).toBe(true); + } + }); + + it('query - queryChildren:true queryComponent:true 同时查询', async () => { + expect(createdNode).not.toBeNull(); + if (createdNode) { + const result = await NodeProxy.query({ + path: createdNode.path, + queryChildren: true, + queryComponent: true, + }) as INodeInfo | null; + expect(result).toBeDefined(); + expect(result?.components).toBeDefined(); + } + }); }); + describe('3. 节点更新操作(依赖创建的节点)', () => { - it('updateNode - 更新节点位置', async () => { + it('update - 更新节点位置', async () => { expect(createdNode).not.toBeNull(); if (createdNode) { const newPosition: IVec3 = { x: 5, y: 5, z: 5 }; @@ -124,7 +173,7 @@ describe('Node Proxy 测试', () => { } }; - const result = await NodeProxy.updateNode(params); + const result = await NodeProxy.update(params); expect(result).toBeDefined(); expect(result?.path).toBe(createdNode.path); @@ -134,12 +183,12 @@ describe('Node Proxy 测试', () => { queryChildren: false, queryComponent: true }; - const updatedNode = await NodeProxy.queryNode(queryParams); + const updatedNode = await NodeProxy.query(queryParams) as INodeInfo | null; expect(updatedNode?.properties.position).toEqual(newPosition); } }); - it('updateNode - 更新节点激活状态', async () => { + it('update - 更新节点激活状态', async () => { expect(createdNode).not.toBeNull(); if (createdNode) { const params: IUpdateNodeParams = { @@ -150,7 +199,7 @@ describe('Node Proxy 测试', () => { } }; - const result = await NodeProxy.updateNode(params); + const result = await NodeProxy.update(params); expect(result).toBeDefined(); // 验证更新是否生效 @@ -159,12 +208,12 @@ describe('Node Proxy 测试', () => { queryChildren: false, queryComponent: true }; - const updatedNode = await NodeProxy.queryNode(queryParams); + const updatedNode = await NodeProxy.query(queryParams) as INodeInfo | null; expect(updatedNode?.properties.active).toBe(false); } }); - it('updateNode - 更新节点旋转和缩放', async () => { + it('update - 更新节点旋转和缩放', async () => { expect(createdNode).not.toBeNull(); if (createdNode) { const newScale: IVec3 = { x: 2, y: 2, z: 2 }; @@ -177,7 +226,7 @@ describe('Node Proxy 测试', () => { } }; - const result = await NodeProxy.updateNode(params); + const result = await NodeProxy.update(params); expect(result).toBeDefined(); // 验证更新是否生效 @@ -186,14 +235,83 @@ describe('Node Proxy 测试', () => { queryChildren: false, queryComponent: true }; - const updatedNode = await NodeProxy.queryNode(queryParams); + const updatedNode = await NodeProxy.query(queryParams) as INodeInfo | null; expect(updatedNode?.properties.scale).toEqual(newScale); } }); + + it('update - 更新节点名称', async () => { + expect(createdNode).not.toBeNull(); + if (createdNode) { + const params: IUpdateNodeParams = { + path: createdNode.path, + name: 'RenamedTestNode', + }; + + const result = await NodeProxy.update(params); + expect(result).toBeDefined(); + + const queryParams: IQueryNodeParams = { + path: result.path, + queryChildren: false, + queryComponent: false, + }; + const updatedNode = await NodeProxy.query(queryParams) as INodeInfo | null; + expect(updatedNode?.name).toBe('RenamedTestNode'); + createdNode = updatedNode; + } + }); + + it('update - 更新节点 mobility', async () => { + expect(createdNode).not.toBeNull(); + if (createdNode) { + const params: IUpdateNodeParams = { + path: createdNode.path, + properties: { + mobility: MobilityMode.Movable, + }, + }; + + const result = await NodeProxy.update(params); + expect(result).toBeDefined(); + + const queryParams: IQueryNodeParams = { + path: createdNode.path, + queryChildren: false, + queryComponent: false, + }; + const updatedNode = await NodeProxy.query(queryParams) as INodeInfo | null; + expect(updatedNode?.properties.mobility).toBe(MobilityMode.Movable); + } + }); + + it('update - 更新节点 layer', async () => { + expect(createdNode).not.toBeNull(); + if (createdNode) { + const targetLayer = 1 << 25; + const params: IUpdateNodeParams = { + path: createdNode.path, + properties: { + layer: targetLayer, + }, + }; + + const result = await NodeProxy.update(params); + expect(result).toBeDefined(); + + const queryParams: IQueryNodeParams = { + path: createdNode.path, + queryChildren: false, + queryComponent: false, + }; + const updatedNode = await NodeProxy.query(queryParams) as INodeInfo | null; + expect(updatedNode?.properties.layer).toBe(targetLayer); + } + }); }); describe('4. 节点删除操作(依赖创建的节点)', () => { - it('deleteNode - 删除节点(不保持世界变换)', async () => { + it('delete - 删除节点(不保持世界变换)', async () => { expect(createdNode).not.toBeNull(); if (createdNode) { const params: IDeleteNodeParams = { @@ -201,7 +319,7 @@ describe('Node Proxy 测试', () => { keepWorldTransform: false }; - const result = await NodeProxy.deleteNode(params); + const result = await NodeProxy.delete(params); expect(result).toBeDefined(); expect(result?.path).toBe(createdNode.path); @@ -211,14 +329,14 @@ describe('Node Proxy 测试', () => { queryChildren: false, queryComponent: true }; - const deletedNode = await NodeProxy.queryNode(queryParams); + const deletedNode = await NodeProxy.query(queryParams) as INodeInfo | null; expect(deletedNode).toBeNull(); createdNode = null; } }); - it('deleteNode - 删除节点(保持世界变换)', async () => { + it('delete - 删除节点(保持世界变换)', async () => { // 先创建一个新节点用于删除测试 const createParams: ICreateByNodeTypeParams = { path: 'NodeToDelete', @@ -227,7 +345,7 @@ describe('Node Proxy 测试', () => { workMode: '3d' }; - const tempNode = await NodeProxy.createNodeByType(createParams); + const tempNode = await NodeProxy.createByType(createParams); expect(tempNode).toBeDefined(); // 删除该节点 @@ -236,25 +354,25 @@ describe('Node Proxy 测试', () => { keepWorldTransform: true }; - const result = await NodeProxy.deleteNode(deleteParams); + const result = await NodeProxy.delete(deleteParams); expect(result).toBeDefined(); expect(result?.path).toBe('NodeToDelete/NodeToDelete'); }); }); describe('5. 边界情况测试', () => { - it('queryNode - 查询不存在的节点应返回null', async () => { + it('query - 查询不存在的节点应返回null', async () => { const params: IQueryNodeParams = { path: '/NonExistentNode', queryChildren: false, queryComponent: false }; - const result = await NodeProxy.queryNode(params); + const result = await NodeProxy.query(params) as INodeInfo | null; expect(result).toBeNull(); }); - it('updateNode - 更新不存在的节点应抛异常', async () => { + it('update - 更新不存在的节点应抛异常', async () => { const params: IUpdateNodeParams = { path: '/NonExistentNode', name: 'NonExistentNode', @@ -263,22 +381,22 @@ describe('Node Proxy 测试', () => { } }; - await expect(NodeProxy.updateNode(params)).rejects.toThrow(); + await expect(NodeProxy.update(params)).rejects.toThrow(); }); - it('deleteNode - 删除不存在的节点应返回null', async () => { + it('delete - 删除不存在的节点应返回null', async () => { const params: IDeleteNodeParams = { path: '/NonExistentNode', keepWorldTransform: false }; - const result = await NodeProxy.deleteNode(params); + const result = await NodeProxy.delete(params); expect(result).toBeNull(); }); }); describe('6. 添加所有内置的节点', () => { - const allNodes: INode[] = []; + const allNodes: INodeInfo[] = []; afterAll(async () => { try { for (const node of allNodes) { @@ -288,7 +406,7 @@ describe('Node Proxy 测试', () => { keepWorldTransform: true }; - const result = await NodeProxy.deleteNode(deleteParams); + const result = await NodeProxy.delete(deleteParams); expect(result).toBeDefined(); expect(result?.path).toBe(node!.path); }; @@ -297,7 +415,7 @@ describe('Node Proxy 测试', () => { throw e; } }); - it('createNode - 创建所有内置节点', async () => { + it('createByType - 创建所有内置节点', async () => { const addCanvas: NodeType[] = [ NodeType.SPRITE, @@ -331,7 +449,7 @@ describe('Node Proxy 测试', () => { continue; } try { - createdNode = await NodeProxy.createNodeByType(params); + createdNode = await NodeProxy.createByType(params); expect(createdNode).toBeDefined(); allNodes.push(createdNode!); @@ -432,7 +550,7 @@ describe('Node Proxy 测试', () => { name: 'TreeTestNode', nodeType: NodeType.EMPTY, }; - const created = await NodeProxy.createNodeByType(createParams); + const created = await NodeProxy.createByType(createParams); expect(created).toBeDefined(); const params: IQueryNodeTreeParams = { path: created!.path }; @@ -442,7 +560,7 @@ describe('Node Proxy 测试', () => { expect(subtree!.isScene).toBe(false); // 清理 - await NodeProxy.deleteNode({ path: created!.path, keepWorldTransform: false }); + await NodeProxy.delete({ path: created!.path, keepWorldTransform: false }); }); it('queryNodeTree - 查询不存在的路径应返回 null', async () => { @@ -458,7 +576,7 @@ describe('Node Proxy 测试', () => { name: 'CompTreeTestNode', nodeType: NodeType.SPRITE, }; - const created = await NodeProxy.createNodeByType(createParams); + const created = await NodeProxy.createByType(createParams); expect(created).toBeDefined(); const tree = await NodeProxy.queryNodeTree({ path: created!.path }); @@ -473,57 +591,57 @@ describe('Node Proxy 测试', () => { } // 清理 - await NodeProxy.deleteNode({ path: created!.path, keepWorldTransform: false }); + await NodeProxy.delete({ path: created!.path, keepWorldTransform: false }); }); }); describe('8. 节点命名规则测试 - 同名节点自动添加后缀', () => { - const createdNodes: INode[] = []; + const createdNodes: INodeInfo[] = []; const parentPath = '/'; afterAll(async () => { for (const node of createdNodes.reverse()) { try { - await NodeProxy.deleteNode({ path: node.path, keepWorldTransform: false }); + await NodeProxy.delete({ path: node.path, keepWorldTransform: false }); } catch (e) { console.log(`删除节点失败: ${node.path}, ${e}`); } } }); - it('createNode - 唯一名称不添加后缀', async () => { + it('createByType - 唯一名称不添加后缀', async () => { const params: ICreateByNodeTypeParams = { path: parentPath, name: 'UniqueNode', nodeType: NodeType.EMPTY, }; - const node = await NodeProxy.createNodeByType(params); + const node = await NodeProxy.createByType(params); expect(node).toBeDefined(); expect(node!.name).toBe('UniqueNode'); expect(node!.path).toBe('UniqueNode'); createdNodes.push(node!); }); - it('createNode - 第二个同名节点添加_001后缀', async () => { + it('createByType - 第二个同名节点添加_001后缀', async () => { const params: ICreateByNodeTypeParams = { path: parentPath, name: 'DupNode', nodeType: NodeType.EMPTY, }; - const node1 = await NodeProxy.createNodeByType(params); + const node1 = await NodeProxy.createByType(params); expect(node1).toBeDefined(); expect(node1!.name).toBe('DupNode'); expect(node1!.path).toBe('DupNode'); createdNodes.push(node1!); - const node2 = await NodeProxy.createNodeByType(params); + const node2 = await NodeProxy.createByType(params); expect(node2).toBeDefined(); expect(node2!.name).toBe('DupNode_001'); expect(node2!.path).toBe('DupNode_001'); createdNodes.push(node2!); }); - it('createNode - 多个同名节点依次添加_001,_002,...后缀', async () => { + it('createByType - 多个同名节点依次添加_001,_002,...后缀', async () => { const totalCount = 5; const baseName = 'MultiDupNode'; for (let i = 0; i < totalCount; i++) { @@ -532,7 +650,7 @@ describe('Node Proxy 测试', () => { name: baseName, nodeType: NodeType.EMPTY, }; - const node = await NodeProxy.createNodeByType(params); + const node = await NodeProxy.createByType(params); expect(node).toBeDefined(); const expectedName = i === 0 ? baseName : `${baseName}_${String(i).padStart(3, '0')}`; expect(node!.name).toBe(expectedName); @@ -541,32 +659,32 @@ describe('Node Proxy 测试', () => { } }); - it('createNode - 删除中间节点后新增应复用已删除的名称', async () => { + it('createByType - 删除中间节点后新增应复用已删除的名称', async () => { const baseName = 'GapNode'; // 添加3个同名节点: GapNode, GapNode_001, GapNode_002 - const node0 = await NodeProxy.createNodeByType({ path: parentPath, name: baseName, nodeType: NodeType.EMPTY }); - const node1 = await NodeProxy.createNodeByType({ path: parentPath, name: baseName, nodeType: NodeType.EMPTY }); - const node2 = await NodeProxy.createNodeByType({ path: parentPath, name: baseName, nodeType: NodeType.EMPTY }); + const node0 = await NodeProxy.createByType({ path: parentPath, name: baseName, nodeType: NodeType.EMPTY }); + const node1 = await NodeProxy.createByType({ path: parentPath, name: baseName, nodeType: NodeType.EMPTY }); + const node2 = await NodeProxy.createByType({ path: parentPath, name: baseName, nodeType: NodeType.EMPTY }); expect(node0!.path).toBe(baseName); expect(node1!.path).toBe(`${baseName}_001`); expect(node2!.path).toBe(`${baseName}_002`); // 删除 _001 - const deleteResult = await NodeProxy.deleteNode({ path: node1!.path, keepWorldTransform: false }); + const deleteResult = await NodeProxy.delete({ path: node1!.path, keepWorldTransform: false }); expect(deleteResult).toBeDefined(); // 再添加2个,第一个应复用 _001,第二个为 _003 - const node3 = await NodeProxy.createNodeByType({ path: parentPath, name: baseName, nodeType: NodeType.EMPTY }); - const node4 = await NodeProxy.createNodeByType({ path: parentPath, name: baseName, nodeType: NodeType.EMPTY }); + const node3 = await NodeProxy.createByType({ path: parentPath, name: baseName, nodeType: NodeType.EMPTY }); + const node4 = await NodeProxy.createByType({ path: parentPath, name: baseName, nodeType: NodeType.EMPTY }); expect(node3!.path).toBe(`${baseName}_001`); expect(node4!.path).toBe(`${baseName}_003`); // 清理 - await NodeProxy.deleteNode({ path: node4!.path, keepWorldTransform: false }); - await NodeProxy.deleteNode({ path: node3!.path, keepWorldTransform: false }); - await NodeProxy.deleteNode({ path: node2!.path, keepWorldTransform: false }); - await NodeProxy.deleteNode({ path: node0!.path, keepWorldTransform: false }); + await NodeProxy.delete({ path: node4!.path, keepWorldTransform: false }); + await NodeProxy.delete({ path: node3!.path, keepWorldTransform: false }); + await NodeProxy.delete({ path: node2!.path, keepWorldTransform: false }); + await NodeProxy.delete({ path: node0!.path, keepWorldTransform: false }); }); }); }); \ No newline at end of file diff --git a/src/core/scene/test/prefab-proxy.testcase.ts b/src/core/scene/test/prefab-proxy.testcase.ts index 0b94a0dae..3e0e79528 100644 --- a/src/core/scene/test/prefab-proxy.testcase.ts +++ b/src/core/scene/test/prefab-proxy.testcase.ts @@ -11,9 +11,9 @@ import type { IGetPrefabInfoParams, ICreateByNodeTypeParams, ICreateByAssetParams, - INode, + INodeInfo, IPrefabInfo, - IComponent + IComponentInfo } from '../common'; import { NodeType } from '../common'; import { ComponentProxy } from '../main-process/proxy/component-proxy'; @@ -34,7 +34,7 @@ describe('Prefab Proxy In Scene 测试', () => { } let testNodePath = ''; - let testNodePrefabNode: INode | null = null;// TestPrefabNode 转换成的 prefab node + let testNodePrefabNode: INodeInfo | null = null;// TestPrefabNode 转换成的 prefab node let duplicateURL = ''; const prefabAssetName = 'TestPrefab'; @@ -85,7 +85,7 @@ describe('Prefab Proxy In Scene 测试', () => { position: { x: 10, y: 20, z: 0 } }; - const testNode = await NodeProxy.createNodeByType(createParams); + const testNode = await NodeProxy.createByType(createParams); expect(testNode).toBeDefined(); expect(testNode?.name).toBe('TestPrefabNode'); if (testNode) { @@ -144,7 +144,7 @@ describe('Prefab Proxy In Scene 测试', () => { name: 'PrefabInstanceNode-CreatePrefabFromNode' }; - const prefabInstanceNode = await NodeProxy.createNodeByAsset(createParams); + const prefabInstanceNode = await NodeProxy.createByAsset(createParams); expect(prefabInstanceNode).toBeDefined(); expect(prefabInstanceNode?.prefab).toBeDefined(); expect(prefabInstanceNode?.prefab?.asset).toBeDefined(); @@ -170,7 +170,7 @@ describe('Prefab Proxy In Scene 测试', () => { position: { x: 10, y: 20, z: 0 } }; - const normalNode = await NodeProxy.createNodeByType(createParams); + const normalNode = await NodeProxy.createByType(createParams); expect(normalNode).toBeTruthy(); const params: IIsPrefabInstanceParams = { @@ -212,7 +212,7 @@ describe('Prefab Proxy In Scene 测试', () => { position: { x: 10, y: 20, z: 0 } }; - const normalNode = await NodeProxy.createNodeByType(createParams); + const normalNode = await NodeProxy.createByType(createParams); expect(normalNode).toBeTruthy(); const params: IGetPrefabInfoParams = { @@ -237,7 +237,7 @@ describe('Prefab Proxy In Scene 测试', () => { it('修改预制体实例与身上组件的属性', async () => { expect(testNodePrefabNode).toBeTruthy(); if (testNodePrefabNode) { - const uNode = await NodeProxy.updateNode({ + const uNode = await NodeProxy.update({ path: testNodePrefabNode.path, properties: { position: position @@ -245,7 +245,7 @@ describe('Prefab Proxy In Scene 测试', () => { }); expect(uNode).toBeTruthy(); - const node = await NodeProxy.queryNode({ path: uNode?.path as string, queryChildren: false, queryComponent: false }); + const node = await NodeProxy.query({ path: uNode?.path as string, queryChildren: false, queryComponent: false }) as INodeInfo | null; expect(node).toBeTruthy(); expect(node?.components?.length).toBeGreaterThan(0); @@ -279,7 +279,7 @@ describe('Prefab Proxy In Scene 测试', () => { position: { x: 10, y: 20, z: 0 } }; - const normalNode = await NodeProxy.createNodeByType(createParams); + const normalNode = await NodeProxy.createByType(createParams); expect(normalNode).toBeTruthy(); if (normalNode) { const params: IApplyPrefabChangesParams = { @@ -306,7 +306,7 @@ describe('Prefab Proxy In Scene 测试', () => { name: 'PrefabInstanceNode-applyPrefabChanges' }; - const prefabInstanceNode = await NodeProxy.createNodeByAsset(createParams); + const prefabInstanceNode = await NodeProxy.createByAsset(createParams); expect(prefabInstanceNode).toBeTruthy(); expect(prefabInstanceNode?.properties.position).toEqual(position); expect(prefabInstanceNode?.components?.length).toBeGreaterThan(0); @@ -314,9 +314,9 @@ describe('Prefab Proxy In Scene 测试', () => { const path = prefabInstanceNode && prefabInstanceNode.components && prefabInstanceNode.components[0].path || ''; expect(path).toBeTruthy(); - const component = await ComponentProxy.queryComponent({ + const component = await ComponentProxy.query({ path: path, - }) as IComponent; + }) as IComponentInfo; expect(prefabInstanceNode).toBeTruthy(); expect(component?.properties.contentSize.value).toEqual(contentSize); @@ -338,7 +338,7 @@ describe('Prefab Proxy In Scene 测试', () => { position: { x: 10, y: 20, z: 0 } }; - const normalNode = await NodeProxy.createNodeByType(createParams); + const normalNode = await NodeProxy.createByType(createParams); expect(normalNode).toBeTruthy(); if (normalNode) { const params: IRevertToPrefabParams = { @@ -354,11 +354,11 @@ describe('Prefab Proxy In Scene 测试', () => { expect(testNodePrefabNode).toBeTruthy(); if (testNodePrefabNode) { - const node = await NodeProxy.queryNode({ path: testNodePrefabNode.path, queryChildren: false, queryComponent: false }); + const node = await NodeProxy.query({ path: testNodePrefabNode.path, queryChildren: false, queryComponent: false }) as INodeInfo | null; expect(node).toBeTruthy(); if (!node) return; - const uNode = await NodeProxy.updateNode({ + const uNode = await NodeProxy.update({ path: testNodePrefabNode.path, properties: { position: position @@ -376,7 +376,7 @@ describe('Prefab Proxy In Scene 测试', () => { const result = await PrefabProxy.revertToPrefab(params); expect(result).toBe(true); - const node2 = await NodeProxy.queryNode({ path: path, queryChildren: false, queryComponent: false }); + const node2 = await NodeProxy.query({ path: path, queryChildren: false, queryComponent: false }) as INodeInfo | null; expect(node.properties.position).toEqual(node2?.properties.position); } }); @@ -390,7 +390,7 @@ describe('Prefab Proxy In Scene 测试', () => { nodeType: NodeType.EMPTY, }; - const testNode = await NodeProxy.createNodeByType(createParams); + const testNode = await NodeProxy.createByType(createParams); expect(testNode).toBeTruthy(); if (!testNode) return; @@ -408,7 +408,7 @@ describe('Prefab Proxy In Scene 测试', () => { const prefabNodePath = prefabNode.path; // 获取初始属性 - const initialQuery = await NodeProxy.queryNode({ path: prefabNodePath, queryChildren: false, queryComponent: false }); + const initialQuery = await NodeProxy.query({ path: prefabNodePath, queryChildren: false, queryComponent: false }) as INodeInfo | null; expect(initialQuery).toBeTruthy(); if (!initialQuery) return; @@ -437,7 +437,7 @@ describe('Prefab Proxy In Scene 测试', () => { }; const appliedName = `${originalName}-Renamed`; - const firstUpdateResult = await NodeProxy.updateNode({ + const firstUpdateResult = await NodeProxy.update({ path: prefabNodePath, name: appliedName, properties: { @@ -479,7 +479,7 @@ describe('Prefab Proxy In Scene 测试', () => { w: 1.2, }; - const secondUpdateResult = await NodeProxy.updateNode({ + const secondUpdateResult = await NodeProxy.update({ path: updatedPrefabNodePath, properties: { scale: overriddenScale, @@ -495,14 +495,14 @@ describe('Prefab Proxy In Scene 测试', () => { nodePath: updatedPrefabNodePath, }; - const queryNode = await NodeProxy.queryNode({ path: updatedPrefabNodePath, queryChildren: false, queryComponent: false }); + const queryNode = await NodeProxy.query({ path: updatedPrefabNodePath, queryChildren: false, queryComponent: false }) as INodeInfo | null; queryNode && console.log(queryNode.properties); const revertResult = await PrefabProxy.revertToPrefab(revertParams); expect(revertResult).toBe(true); // 验证还原后的属性 - const revertedQuery = await NodeProxy.queryNode({ path: updatedPrefabNodePath, queryChildren: false, queryComponent: false }); + const revertedQuery = await NodeProxy.query({ path: updatedPrefabNodePath, queryChildren: false, queryComponent: false }) as INodeInfo | null; expect(revertedQuery).toBeTruthy(); if (!revertedQuery) return; @@ -529,7 +529,7 @@ describe('Prefab Proxy In Scene 测试', () => { nodeType: NodeType.EMPTY, }; - const parentNode = await NodeProxy.createNodeByType(createParentParams); + const parentNode = await NodeProxy.createByType(createParentParams); expect(parentNode).toBeTruthy(); if (!parentNode) return; @@ -541,7 +541,7 @@ describe('Prefab Proxy In Scene 测试', () => { nodeType: NodeType.EMPTY, }; - const childNode = await NodeProxy.createNodeByType(createChildParams); + const childNode = await NodeProxy.createByType(createChildParams); expect(childNode).toBeTruthy(); if (!childNode) return; @@ -559,11 +559,11 @@ describe('Prefab Proxy In Scene 测试', () => { const prefabNodePath = prefabNode.path; // 查询节点及其子节点,确认子节点存在(创建预制体后,父节点名称已改变,子节点 path 也会改变) - const beforeRevertQuery = await NodeProxy.queryNode({ + const beforeRevertQuery = await NodeProxy.query({ path: prefabNodePath, queryChildren: true, queryComponent: false - }); + }) as INodeInfo | null; expect(beforeRevertQuery).toBeTruthy(); if (!beforeRevertQuery) return; @@ -583,7 +583,7 @@ describe('Prefab Proxy In Scene 测试', () => { expect(originalChildPath).not.toBe(''); // 修改父节点属性 - const updateResult = await NodeProxy.updateNode({ + const updateResult = await NodeProxy.update({ path: prefabNodePath, properties: { position: { x: 100, y: 100, z: 100 }, @@ -600,11 +600,11 @@ describe('Prefab Proxy In Scene 测试', () => { expect(revertResult).toBe(true); // 查询节点及其子节点,验证子节点的 path 保持不变 - const afterRevertQuery = await NodeProxy.queryNode({ + const afterRevertQuery = await NodeProxy.query({ path: prefabNodePath, queryChildren: true, queryComponent: false - }); + }) as INodeInfo | null; expect(afterRevertQuery).toBeTruthy(); if (!afterRevertQuery) return; @@ -633,7 +633,7 @@ describe('Prefab Proxy In Scene 测试', () => { recursive: true }; - const unpackedNode: INode | null = await PrefabProxy.unpackPrefabInstance(params); + const unpackedNode: INodeInfo | null = await PrefabProxy.unpackPrefabInstance(params); expect(unpackedNode).toBeTruthy(); if (!unpackedNode) return; @@ -663,7 +663,7 @@ describe('Prefab Proxy In Scene 测试', () => { name: 'PrefabInstanceNode2' }; - const prefabInstance2 = await NodeProxy.createNodeByAsset(createParams); + const prefabInstance2 = await NodeProxy.createByAsset(createParams); expect(prefabInstance2).toBeDefined(); if (prefabInstance2) { @@ -687,7 +687,7 @@ describe('Prefab Proxy In Scene 测试', () => { nodeType: NodeType.EMPTY, }; - const normalNode = await NodeProxy.createNodeByType(createParams); + const normalNode = await NodeProxy.createByType(createParams); expect(normalNode).toBeTruthy(); if (!normalNode) return; @@ -711,7 +711,7 @@ describe('Prefab Proxy In Scene 测试', () => { nodeType: NodeType.EMPTY, }; - const testNode = await NodeProxy.createNodeByType(createNodeParams); + const testNode = await NodeProxy.createByType(createNodeParams); expect(testNode).toBeTruthy(); if (!testNode) return; @@ -758,7 +758,7 @@ describe('Prefab Proxy In Scene 测试', () => { nodeType: NodeType.EMPTY, }; - const anotherNode = await NodeProxy.createNodeByType(anotherNodeParams); + const anotherNode = await NodeProxy.createByType(anotherNodeParams); expect(anotherNode).toBeTruthy(); if (!anotherNode) return; @@ -794,7 +794,7 @@ describe('Prefab Proxy In Scene 测试', () => { }; const renamedNode = `${nodeName}-Renamed`; - const initialUpdateResult = await NodeProxy.updateNode({ + const initialUpdateResult = await NodeProxy.update({ path: nodePath, name: renamedNode, properties: { @@ -830,7 +830,7 @@ describe('Prefab Proxy In Scene 测试', () => { w: 0.7071068, }; - const secondUpdateResult = await NodeProxy.updateNode({ + const secondUpdateResult = await NodeProxy.update({ path: nodePath, properties: { position: changedPosition, @@ -852,7 +852,7 @@ describe('Prefab Proxy In Scene 测试', () => { expect(revertResult).toBe(true); // 验证还原后的属性 - const queryNodeResult = await NodeProxy.queryNode({ path: nodePath, queryChildren: false, queryComponent: false }); + const queryNodeResult = await NodeProxy.query({ path: nodePath, queryChildren: false, queryComponent: false }) as INodeInfo | null; expect(queryNodeResult).not.toBeNull(); if (queryNodeResult) { const props = queryNodeResult.properties; @@ -895,7 +895,7 @@ describe('Prefab Proxy In Scene 测试', () => { nodeType: NodeType.EMPTY, }; - const parentNode = await NodeProxy.createNodeByType(parentNodeParams); + const parentNode = await NodeProxy.createByType(parentNodeParams); expect(parentNode).toBeTruthy(); if (!parentNode) return; @@ -907,7 +907,7 @@ describe('Prefab Proxy In Scene 测试', () => { nodeType: NodeType.EMPTY, }; - const childNode = await NodeProxy.createNodeByType(childNodeParams); + const childNode = await NodeProxy.createByType(childNodeParams); expect(childNode).toBeTruthy(); // 从父节点创建预制体(包含子节点) @@ -971,7 +971,7 @@ describe('Prefab Proxy In Scene 测试', () => { }); it('测试重复创建预制体(覆盖测试)', async () => { - const node = await NodeProxy.createNodeByType({ + const node = await NodeProxy.createByType({ path: '', nodeType: NodeType.EMPTY, name: 'Duplicate-Node' @@ -1015,7 +1015,7 @@ describe('Prefab Proxy In Scene 测试', () => { nodeType: NodeType.EMPTY, }; - const testNode = await NodeProxy.createNodeByType(createNodeParams); + const testNode = await NodeProxy.createByType(createNodeParams); expect(testNode).toBeTruthy(); if (!testNode) return; @@ -1058,7 +1058,7 @@ describe('Prefab Proxy In Scene 测试', () => { nodeType: NodeType.EMPTY, }; - const testNode = await NodeProxy.createNodeByType(createNodeParams); + const testNode = await NodeProxy.createByType(createNodeParams); expect(testNode).toBeTruthy(); if (!testNode) return; @@ -1079,7 +1079,7 @@ describe('Prefab Proxy In Scene 测试', () => { // 多次修改和应用 for (let i = 0; i < 3; i++) { - const updateResult = await NodeProxy.updateNode({ + const updateResult = await NodeProxy.update({ path: prefabNodePath, properties: { position: { @@ -1100,7 +1100,7 @@ describe('Prefab Proxy In Scene 测试', () => { } // 修改后还原 - const finalUpdateResult = await NodeProxy.updateNode({ + const finalUpdateResult = await NodeProxy.update({ path: prefabNodePath, properties: { position: { x: 999, y: 999, z: basePos.z ?? 0 }, diff --git a/src/core/scene/test/scene.test.ts b/src/core/scene/test/scene.test.ts index 425c3ff50..ea7a2fc5d 100644 --- a/src/core/scene/test/scene.test.ts +++ b/src/core/scene/test/scene.test.ts @@ -15,6 +15,8 @@ beforeAll(async () => { console.log('创建场景测试目录:', SceneTestEnv.cacheDirectory); const TestUtils = await import('../../test/global-setup'); await TestUtils.globalSetup(); + const { loadSceneI18n } = await import('../index'); + await loadSceneI18n(); }); afterAll(async () => { @@ -24,7 +26,9 @@ afterAll(async () => { import './editor-proxy-scene.testcase'; import './editor-proxy-prefab.testcase'; import './node-proxy.testcase'; +import './node-for-editor.testcase'; import './component-proxy.testcase'; +import './component-for-editor.testcase'; import './prefab-proxy.testcase'; import './script-proxy.testcase'; import './engine-proxy.testcase'; diff --git a/tests/__snapshots__/dts-snapshot.test.ts.snap b/tests/__snapshots__/dts-snapshot.test.ts.snap index de58824e8..30d9fa53f 100644 --- a/tests/__snapshots__/dts-snapshot.test.ts.snap +++ b/tests/__snapshots__/dts-snapshot.test.ts.snap @@ -5150,7 +5150,7 @@ export declare interface GlTFUserData { }; } export declare interface IAddComponentOptions { - nodePathOrUuid: string; + nodePath: string; component: string; } export declare interface IApplyPrefabChangesParams { @@ -5280,13 +5280,7 @@ export declare interface ICLI { export declare interface ICloseOptions { urlOrUUID?: string; } -export declare interface IComponent extends IComponentIdentifier { - properties: { - [key: string]: IPropertyValueType; - }; - prefab: ICompPrefabInfo | null; -} -export declare interface IComponentForEditor extends IProperty { +export declare interface IComponent extends IProperty { value: { enabled: IPropertyValueType; uuid: IPropertyValueType; @@ -5294,34 +5288,22 @@ export declare interface IComponentForEditor extends IProperty { } & Record; mountedRoot?: string; } -export declare interface IComponentIdentifier { - cid: string; - path: string; - uuid: string; - name: string; - type: string; - enabled: boolean; -} export declare interface IComponentService extends IServiceEvents { - addComponent(params: IAddComponentOptions): Promise; - removeComponent(params: IRemoveComponentOptions): Promise; - setProperty(params: ISetPropertyOptions | ISetPropertyOptionsForEditor): Promise; - queryComponent(params: IQueryComponentOptions | string): Promise; - queryAllComponent(): Promise; - createComponent(params: IAddComponentOptions): Promise; - resetComponent(params: IQueryComponentOptions): Promise; + add(params: IAddComponentOptions): Promise; + remove(params: IRemoveComponentOptions): Promise; + setProperty(params: ISetPropertyOptions): Promise; + query(params: IQueryComponentOptions | string): Promise; + queryAll(): Promise; + reset(params: IQueryComponentOptions): Promise; queryClasses(options?: IQueryClassesOptions): Promise<{ name: string; }[]>; - queryComponentFunctionOfNode(uuid: string): Promise; - executeComponentMethod(options: IExecuteComponentMethodOptions): Promise; - queryComponentHasScript(name: string): Promise; + queryFunctionOfNode(path: string): Promise; + executeMethod(options: IExecuteComponentMethodOptions): Promise; + hasScript(name: string): Promise; init(): void; unregisterCompMgrEvents(): void; } -export declare interface ICompPrefabInfo { - fileId: string; -} export declare interface ICreateByAssetParams extends IBaseCreateNodeParams { dbURL: string; } @@ -5365,7 +5347,7 @@ export declare interface IEngineService extends IServiceEvents { repaintInEditMode(): Promise; } export declare interface IExecuteComponentMethodOptions { - uuid: string; + path: string; name: string; args: any[]; } @@ -5421,24 +5403,25 @@ export declare interface ImageMeta { uri?: string; remap?: string; } -export declare interface IMountedChildrenInfo { - targetInfo: ITargetInfo | null; - nodes: INodeIdentifier[]; -} -export declare interface IMountedComponentsInfo { - targetInfo: ITargetInfo | null; - components: IComponentIdentifier[]; -} -export declare interface INode extends INodeIdentifier { - properties: INodeProperties; - components?: IComponent[] | IComponentIdentifier[]; - children?: INode[]; - prefab: IPrefabInfo | null; -} -export declare interface INodeIdentifier { - nodeId: string; +export declare interface INode { path: string; - name: string; + active: IProperty; + locked: IProperty; + name: IProperty; + position: IProperty; + rotation: IProperty; + mobility: IProperty; + scale: IProperty; + layer: IProperty; + uuid: IProperty; + children: IProperty[]; + parent: IProperty; + __comps__: IProperty[]; + __type__: string; + __prefab__?: IPrefab; + _prefabInstance?: any; + removedComponents?: IRemovedComponentInfo[]; + mountedRoot?: string; } export declare interface INodeProperties { position: IVec3; @@ -5450,12 +5433,19 @@ export declare interface INodeProperties { active: boolean; } export declare interface INodeService extends IServiceEvents { - createNodeByType(params: ICreateByNodeTypeParams): Promise; - createNodeByAsset(params: ICreateByAssetParams): Promise; - deleteNode(params: IDeleteNodeParams): Promise; - updateNode(params: IUpdateNodeParams): Promise; - queryNode(params: IQueryNodeParams): Promise; + createByType(params: ICreateByNodeTypeParams): Promise; + createByAsset(params: ICreateByAssetParams): Promise; + delete(params: IDeleteNodeParams): Promise; + update(params: IUpdateNodeParams): Promise; + query(params?: IQueryNodeParams): Promise; queryNodeTree(params: IQueryNodeTreeParams): Promise; + previewSetProperty(options: ISetPropertyOptions): Promise; + cancelPreviewSetProperty(options: ISetPropertyOptions): Promise; + setProperty(options: ISetPropertyOptions): Promise; + reset(path: string): Promise; + resetProperty(options: ISetPropertyOptions): Promise; + updatePropertyFromNull(options: ISetPropertyOptions): Promise; + setNodeAndChildrenLayer(options: ISetPropertyOptions): Promise; } export declare interface INodeTreeComponent { isCustom: boolean; @@ -5491,27 +5481,13 @@ export declare interface IOperationService { changePointer(type: string): void; } export declare interface IPrefab { - name: string; uuid: string; - data: INodeIdentifier; - optimizationPolicy: OptimizationPolicy; - persistent: boolean; -} -export declare interface IPrefabInfo { - asset?: IPrefab; - root?: INodeIdentifier; - instance?: IPrefabInstance; fileId: string; - targetOverrides: ITargetOverrideInfo[]; - nestedPrefabInstanceRoots: INodeIdentifier[]; -} -export declare interface IPrefabInstance { - fileId: string; - prefabRootNode?: INodeIdentifier; - mountedChildren: IMountedChildrenInfo[]; - mountedComponents: IMountedComponentsInfo[]; - propertyOverrides: IPropertyOverrideInfo[]; - removedComponents: ITargetInfo[]; + rootUuid: string; + sync: boolean; + prefabStateInfo: IPrefabStateInfo; + targetOverrides?: ITargetOverrideInfo[]; + instance?: IProperty; } export declare interface IPrefabService extends IServiceEvents { createPrefabFromNode(params: ICreatePrefabFromNodeParams): Promise; @@ -5519,7 +5495,7 @@ export declare interface IPrefabService extends IServiceEvents { revertToPrefab(params: IRevertToPrefabParams): Promise; unpackPrefabInstance(params: IUnpackPrefabInstanceParams): Promise; isPrefabInstance(params: IIsPrefabInstanceParams): Promise; - getPrefabInfo(params: IGetPrefabInfoParams): Promise; + getPrefabInfo(params: IGetPrefabInfoParams): Promise; removePrefabInfoFromNode(node: Node_2, removeNested?: boolean): void; } export declare interface IPrefabStateInfo { @@ -5543,7 +5519,7 @@ export declare interface IProperty { visible?: boolean; name?: string; elementTypeData?: IProperty; - path?: string; + path: string; isArray?: boolean; invalid?: boolean; extends?: string[]; @@ -5577,11 +5553,6 @@ export declare type IPropertyLock = { default: number; message: string }; -export declare interface IPropertyOverrideInfo { - targetInfo: ITargetInfo | null; - propertyPath: string[]; - value?: any; -} export declare type IPropertyValueType = IProperty | IProperty[] | null | undefined | number | boolean | string | Vec4 | Vec3 | Vec2 | Mat4 | any | Array export declare interface IQuat { x: number; @@ -5614,17 +5585,29 @@ export declare interface IReloadOptions { export declare interface IRemoveComponentOptions { path: string; } +export declare interface IRemovedComponentInfo { + name: string; + fileID: string; +} export declare interface IRevertToPrefabParams { nodePath: string; } export declare interface ISaveOptions { urlOrUUID?: string; } -export declare interface IScene extends IBaseIdentifier { - name: string; - prefab: IPrefabInfo | null; - children: INode[]; - components: IComponentIdentifier[]; +export declare interface IScene { + path: string; + name: IProperty; + active: IProperty; + locked: IProperty; + _globals: Record; + isScene: boolean; + autoReleaseAssets: IProperty; + uuid: IProperty; + children: IProperty[]; + parent: string; + __type__: string; + targetOverrides?: ITargetOverrideInfo[]; } export declare interface ISceneMouseEvent { x: number; @@ -5700,10 +5683,9 @@ export declare interface IServiceEvents { onSetPropertyComponent?(comp: Component): void; onComponentAdded?(comp: Component): void; onComponentRemoved?(comp: Component): void; + onBeforeChangeComponent?(node: Node_2): void; + onBeforeAddComponent?(name: string, node: Node_2): void; onBeforeRemoveComponent?(comp: Component): void; - onComponentBeforeChanged?(node: Node_2): void; - onBeforeComponentAdded?(name: string, node: Node_2): void; - onComponentChanged?(name: string, opts: IChangeNodeOptions): void; onAssetDeleted?(uuid: string): void; onAssetChanged?(uuid: string): void; onAssetRefreshed?(uuid: string): void; @@ -5728,14 +5710,7 @@ export declare interface IServiceManager { SceneView: ISceneViewService; } export declare interface ISetPropertyOptions { - componentPath: string; - properties: { - [key: string]: null | undefined | number | boolean | string | object | Array; - }; - record?: boolean; -} -export declare interface ISetPropertyOptionsForEditor { - uuid: string; + nodePath: string; path: string; dump: IProperty; record?: boolean; @@ -5757,15 +5732,12 @@ export declare type ISupportCreateCCType = | 'cc.AnimationMask' | 'cc.AnimationGraphVariant'; export declare type ISupportCreateType = typeof SUPPORT_CREATE_TYPES[number]; -export declare interface ITargetInfo { - localID: string[]; -} export declare interface ITargetOverrideInfo { - source: IComponentIdentifier | INodeIdentifier | null; - sourceInfo: ITargetInfo | null; + source: string; + sourceInfo?: string[]; propertyPath: string[]; - target: INodeIdentifier | null; - targetInfo: ITargetInfo | null; + target: string; + targetInfo?: string[]; } export declare interface IUndoService { beginRecording(uuids: string[], options?: any): string; @@ -5939,11 +5911,6 @@ export declare enum NormalImportSetting { recalculate = 3 } export declare type OperationEvent = SceneDragEvent | SceneKeyboardEvent | SceneMouseEvent | 'resize'; -export declare enum OptimizationPolicy { - AUTO = 0, - SINGLE_INSTANCE = 1, - MULTI_INSTANCE = 2 -} export declare interface ParticleAssetUserData { totalParticles: number; life: number; @@ -6969,7 +6936,7 @@ export declare namespace i18n { } } export declare interface IAddComponentOptions { - nodePathOrUuid: string; + nodePath: string; component: string; } export declare interface IApplyPrefabChangesParams { @@ -7150,13 +7117,7 @@ export declare interface ICocosConfigurationPropertySchema { export declare interface ICollisionMatrix { [x: string]: number; } -export declare interface IComponent extends IComponentIdentifier { - properties: { - [key: string]: IPropertyValueType; - }; - prefab: ICompPrefabInfo | null; -} -export declare interface IComponentForEditor extends IProperty { +export declare interface IComponent extends IProperty { value: { enabled: IPropertyValueType; uuid: IPropertyValueType; @@ -7164,34 +7125,22 @@ export declare interface IComponentForEditor extends IProperty { } & Record; mountedRoot?: string; } -export declare interface IComponentIdentifier { - cid: string; - path: string; - uuid: string; - name: string; - type: string; - enabled: boolean; -} export declare interface IComponentService extends IServiceEvents { - addComponent(params: IAddComponentOptions): Promise; - removeComponent(params: IRemoveComponentOptions): Promise; - setProperty(params: ISetPropertyOptions | ISetPropertyOptionsForEditor): Promise; - queryComponent(params: IQueryComponentOptions | string): Promise; - queryAllComponent(): Promise; - createComponent(params: IAddComponentOptions): Promise; - resetComponent(params: IQueryComponentOptions): Promise; + add(params: IAddComponentOptions): Promise; + remove(params: IRemoveComponentOptions): Promise; + setProperty(params: ISetPropertyOptions): Promise; + query(params: IQueryComponentOptions | string): Promise; + queryAll(): Promise; + reset(params: IQueryComponentOptions): Promise; queryClasses(options?: IQueryClassesOptions): Promise<{ name: string; }[]>; - queryComponentFunctionOfNode(uuid: string): Promise; - executeComponentMethod(options: IExecuteComponentMethodOptions): Promise; - queryComponentHasScript(name: string): Promise; + queryFunctionOfNode(path: string): Promise; + executeMethod(options: IExecuteComponentMethodOptions): Promise; + hasScript(name: string): Promise; init(): void; unregisterCompMgrEvents(): void; } -export declare interface ICompPrefabInfo { - fileId: string; -} export declare interface IConfiguration { [key: string]: any; } @@ -7316,7 +7265,7 @@ export declare interface IEngineService extends IServiceEvents { repaintInEditMode(): Promise; } export declare interface IExecuteComponentMethodOptions { - uuid: string; + path: string; name: string; args: any[]; } @@ -7419,14 +7368,6 @@ export declare interface IModuleConfig { } export declare type IModuleItem = IFeatureItem | IFeatureGroup; export declare type IModules = Record; -export declare interface IMountedChildrenInfo { - targetInfo: ITargetInfo | null; - nodes: INodeIdentifier[]; -} -export declare interface IMountedComponentsInfo { - targetInfo: ITargetInfo | null; - components: IComponentIdentifier[]; -} export declare function importAsset(source: string, target: string, options?: AssetOperationOption): Promise; export declare interface ImportMap { imports?: Record; @@ -7441,16 +7382,25 @@ export declare function init_6(): Promise; export declare function init_7(projectPath: string): Promise; export declare function initEngine(enginePath: string, projectPath: string, serverURL?: string): Promise; export declare function initProgrammingFacet(): Promise; -export declare interface INode extends INodeIdentifier { - properties: INodeProperties; - components?: IComponent[] | IComponentIdentifier[]; - children?: INode[]; - prefab: IPrefabInfo | null; -} -export declare interface INodeIdentifier { - nodeId: string; +export declare interface INode { path: string; - name: string; + active: IProperty; + locked: IProperty; + name: IProperty; + position: IProperty; + rotation: IProperty; + mobility: IProperty; + scale: IProperty; + layer: IProperty; + uuid: IProperty; + children: IProperty[]; + parent: IProperty; + __comps__: IProperty[]; + __type__: string; + __prefab__?: IPrefab; + _prefabInstance?: any; + removedComponents?: IRemovedComponentInfo[]; + mountedRoot?: string; } export declare interface INodeProperties { position: IVec3; @@ -7462,12 +7412,19 @@ export declare interface INodeProperties { active: boolean; } export declare interface INodeService extends IServiceEvents { - createNodeByType(params: ICreateByNodeTypeParams): Promise; - createNodeByAsset(params: ICreateByAssetParams): Promise; - deleteNode(params: IDeleteNodeParams): Promise; - updateNode(params: IUpdateNodeParams): Promise; - queryNode(params: IQueryNodeParams): Promise; + createByType(params: ICreateByNodeTypeParams): Promise; + createByAsset(params: ICreateByAssetParams): Promise; + delete(params: IDeleteNodeParams): Promise; + update(params: IUpdateNodeParams): Promise; + query(params?: IQueryNodeParams): Promise; queryNodeTree(params: IQueryNodeTreeParams): Promise; + previewSetProperty(options: ISetPropertyOptions): Promise; + cancelPreviewSetProperty(options: ISetPropertyOptions): Promise; + setProperty(options: ISetPropertyOptions): Promise; + reset(path: string): Promise; + resetProperty(options: ISetPropertyOptions): Promise; + updatePropertyFromNull(options: ISetPropertyOptions): Promise; + setNodeAndChildrenLayer(options: ISetPropertyOptions): Promise; } export declare interface INodeTreeComponent { isCustom: boolean; @@ -7530,27 +7487,13 @@ export declare interface IPluginScriptInfo extends PluginScriptInfo { url: string; } export declare interface IPrefab { - name: string; uuid: string; - data: INodeIdentifier; - optimizationPolicy: OptimizationPolicy; - persistent: boolean; -} -export declare interface IPrefabInfo { - asset?: IPrefab; - root?: INodeIdentifier; - instance?: IPrefabInstance; - fileId: string; - targetOverrides: ITargetOverrideInfo[]; - nestedPrefabInstanceRoots: INodeIdentifier[]; -} -export declare interface IPrefabInstance { fileId: string; - prefabRootNode?: INodeIdentifier; - mountedChildren: IMountedChildrenInfo[]; - mountedComponents: IMountedComponentsInfo[]; - propertyOverrides: IPropertyOverrideInfo[]; - removedComponents: ITargetInfo[]; + rootUuid: string; + sync: boolean; + prefabStateInfo: IPrefabStateInfo; + targetOverrides?: ITargetOverrideInfo[]; + instance?: IProperty; } export declare interface IPrefabService extends IServiceEvents { createPrefabFromNode(params: ICreatePrefabFromNodeParams): Promise; @@ -7558,7 +7501,7 @@ export declare interface IPrefabService extends IServiceEvents { revertToPrefab(params: IRevertToPrefabParams): Promise; unpackPrefabInstance(params: IUnpackPrefabInstanceParams): Promise; isPrefabInstance(params: IIsPrefabInstanceParams): Promise; - getPrefabInfo(params: IGetPrefabInfoParams): Promise; + getPrefabInfo(params: IGetPrefabInfoParams): Promise; removePrefabInfoFromNode(node: Node_2, removeNested?: boolean): void; } export declare interface IPrefabStateInfo { @@ -7595,7 +7538,7 @@ export declare interface IProperty { visible?: boolean; name?: string; elementTypeData?: IProperty; - path?: string; + path: string; isArray?: boolean; invalid?: boolean; extends?: string[]; @@ -7629,11 +7572,6 @@ export declare type IPropertyLock = { default: number; message: string }; -export declare interface IPropertyOverrideInfo { - targetInfo: ITargetInfo | null; - propertyPath: string[]; - value?: any; -} export declare type IPropertyValueType = IProperty | IProperty[] | null | undefined | number | boolean | string | Vec4 | Vec3 | Vec2 | Mat4 | any | Array export declare interface IQuat { x: number; @@ -7666,17 +7604,29 @@ export declare interface IReloadOptions { export declare interface IRemoveComponentOptions { path: string; } +export declare interface IRemovedComponentInfo { + name: string; + fileID: string; +} export declare interface IRevertToPrefabParams { nodePath: string; } export declare interface ISaveOptions { urlOrUUID?: string; } -export declare interface IScene extends IBaseIdentifier { - name: string; - prefab: IPrefabInfo | null; - children: INode[]; - components: IComponentIdentifier[]; +export declare interface IScene { + path: string; + name: IProperty; + active: IProperty; + locked: IProperty; + _globals: Record; + isScene: boolean; + autoReleaseAssets: IProperty; + uuid: IProperty; + children: IProperty[]; + parent: string; + __type__: string; + targetOverrides?: ITargetOverrideInfo[]; } export declare interface ISceneMouseEvent { x: number; @@ -7752,10 +7702,9 @@ export declare interface IServiceEvents { onSetPropertyComponent?(comp: Component): void; onComponentAdded?(comp: Component): void; onComponentRemoved?(comp: Component): void; + onBeforeChangeComponent?(node: Node_2): void; + onBeforeAddComponent?(name: string, node: Node_2): void; onBeforeRemoveComponent?(comp: Component): void; - onComponentBeforeChanged?(node: Node_2): void; - onBeforeComponentAdded?(name: string, node: Node_2): void; - onComponentChanged?(name: string, opts: IChangeNodeOptions): void; onAssetDeleted?(uuid: string): void; onAssetChanged?(uuid: string): void; onAssetRefreshed?(uuid: string): void; @@ -7780,14 +7729,7 @@ export declare interface IServiceManager { SceneView: ISceneViewService; } export declare interface ISetPropertyOptions { - componentPath: string; - properties: { - [key: string]: null | undefined | number | boolean | string | object | Array; - }; - record?: boolean; -} -export declare interface ISetPropertyOptionsForEditor { - uuid: string; + nodePath: string; path: string; dump: IProperty; record?: boolean; @@ -7840,15 +7782,12 @@ export declare type ISupportCreateCCType = | 'cc.AnimationMask' | 'cc.AnimationGraphVariant'; export declare type ISupportCreateType = typeof SUPPORT_CREATE_TYPES[number]; -export declare interface ITargetInfo { - localID: string[]; -} export declare interface ITargetOverrideInfo { - source: IComponentIdentifier | INodeIdentifier | null; - sourceInfo: ITargetInfo | null; + source: string; + sourceInfo?: string[]; propertyPath: string[]; - target: INodeIdentifier | null; - targetInfo: ITargetInfo | null; + target: string; + targetInfo?: string[]; } export declare interface IUerDataConfigItem { key?: string; @@ -8084,11 +8023,6 @@ export declare function onProgress(listener: (current: number, total: number, ur export declare function onReady(listener: () => void): () => void; export declare function open_2(projectPath: string): Promise; export declare type OperationEvent = SceneDragEvent | SceneKeyboardEvent | SceneMouseEvent | 'resize'; -export declare enum OptimizationPolicy { - AUTO = 0, - SINGLE_INSTANCE = 1, - MULTI_INSTANCE = 2 -} export declare interface ParticleAssetUserData { totalParticles: number; life: number;