Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/core/scene/common/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,34 @@ import type { Node } from 'cc';
import { IComponent, IComponentIdentifier } from './component';
import { IVec3, IQuat } from './value-types';
import { IServiceEvents } from '../scene-process/service/core';
import { IPrefabInfo } from './prefab';
import { IPrefabInfo, IPrefabStateInfo } from './prefab';

// ====== Hierarchy tree types (for queryNodeTree) ======

export interface INodeTreeComponent {
isCustom: boolean;
type: string;
value: string;
extends: string[];
}

export interface INodeTreeItem {
name: string;
active: boolean;
locked: boolean;
type: string;
children: INodeTreeItem[];
prefab: IPrefabStateInfo;
parent: string;
path: string;
isScene: boolean;
readonly: boolean;
components: INodeTreeComponent[];
}

export interface IQueryNodeTreeParams {
path?: string;
}

export enum NodeType {
EMPTY = 'Empty', // 空节点
Expand Down Expand Up @@ -219,6 +246,11 @@ export interface INodeService extends IServiceEvents {
* 查询节点
*/
queryNode(params: IQueryNodeParams): Promise<INode | null>;

/**
* 查询节点树(层级管理器格式)
*/
queryNodeTree(params: IQueryNodeTreeParams): Promise<INodeTreeItem | null>;
}

///
Expand Down
21 changes: 19 additions & 2 deletions src/core/scene/common/prefab/prefab-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { IComponentIdentifier } from '../component';

export enum OptimizationPolicy {
AUTO = 0,
SINGLE_INSTANCE = 0,
MULTI_INSTANCE = 1,
SINGLE_INSTANCE = 1,
MULTI_INSTANCE = 2,
}

export interface IPrefabInstance {
Expand Down Expand Up @@ -65,3 +65,20 @@ export interface IPrefabInfo {
targetOverrides: ITargetOverrideInfo[];
nestedPrefabInstanceRoots: INodeIdentifier[];
}

export enum PrefabState {
NotAPrefab = 0, // Normal node, not a Prefab
PrefabChild = 1, // Child node of a Prefab, without PrefabInstance
PrefabInstance = 2, // Root node of a Prefab that contains a PrefabInstance
PrefabLostAsset = 3, // Prefab node with missing asset
}

export interface IPrefabStateInfo {
state: PrefabState;
isUnwrappable: boolean;
isRevertable: boolean;
isApplicable: boolean;
isAddedChild: boolean;
isNested: boolean;
assetUuid: string;
}
2 changes: 1 addition & 1 deletion src/core/scene/common/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ export interface IScriptService extends IServiceEvents {
scriptChange(): Promise<void>;
queryScriptCid(uuid: string): Promise<string | null>;
queryScriptName(uuid: string): Promise<string | null>;
isCustomComponent(classConstructor: Function): Promise<boolean>;
isCustomComponent(classConstructor: Function): boolean;
suspend(condition: Promise<any>): void;
}
5 changes: 5 additions & 0 deletions src/core/scene/main-process/proxy/node-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
INode,
INodeTreeItem,
ICreateByNodeTypeParams,
ICreateByAssetParams,
IQueryNodeParams,
IQueryNodeTreeParams,
IUpdateNodeParams,
IDeleteNodeParams,
IUpdateNodeResult,
Expand All @@ -26,5 +28,8 @@ export const NodeProxy: IPublicNodeService = {
},
queryNode(params: IQueryNodeParams): Promise<INode | null> {
return Rpc.getInstance().request('Node', 'queryNode', [params]);
},
queryNodeTree(params: IQueryNodeTreeParams): Promise<INodeTreeItem | null> {
return Rpc.getInstance().request('Node', 'queryNodeTree', [params]);
}
};
63 changes: 62 additions & 1 deletion src/core/scene/scene-process/service/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
type INode,
type INodeService,
type IQueryNodeParams,
type IQueryNodeTreeParams,
type INodeTreeItem,
type INodeEvents,
type IUpdateNodeParams,
type IUpdateNodeResult,
Expand All @@ -16,10 +18,11 @@ import {
IChangeNodeOptions
} from '../../common';
import { Rpc } from '../rpc';
import { CCObject, Node, Prefab, Quat, Vec3, TransformBit, UITransform, LODGroup } from 'cc';
import { CCClass, CCObject, Node, Prefab, Quat, Vec3, TransformBit, UITransform, LODGroup } from 'cc';
import { createNodeByAsset, loadAny } from './node/node-create';
import { getUICanvasNode, isEditorNode, setLayer } from './node/node-utils';
import { sceneUtils } from './scene/utils';
import { prefabUtils } from './prefab/utils';
import NodeConfig from './node/node-type-config';

const NodeMgr = EditorExtends.Node;
Expand Down Expand Up @@ -405,6 +408,64 @@ export class NodeService extends BaseService<INodeEvents> implements INodeServic
}
}

async queryNodeTree(params: IQueryNodeTreeParams): Promise<INodeTreeItem | null> {
try {
await Service.Editor.lock();
const root = Service.Editor.getRootNode();
if (!root) {
throw new Error('Failed to query node tree: the scene is not opened.');
}

const step = (node: Node): INodeTreeItem | null => {
if (node.objFlags & CCObject.Flags.HideInHierarchy) {
return null;
}

const children = node.children.map(step).filter(Boolean) as INodeTreeItem[];
const prefabStateInfo = prefabUtils.getPrefabStateInfo(node);
const isScene = node.constructor.name === 'Scene';

return {
name: !node.name && isScene ? 'Scene' : node.name,
active: node.active,
locked: Boolean(node.objFlags & CCObject.Flags.LockedInEditor),
type: 'cc.' + node.constructor.name,
children,
prefab: prefabStateInfo,
parent: (node.parent && node.parent.uuid) || '',
path: isScene ? '/' : NodeMgr.getNodePath(node),
isScene,
readonly: false,
components: node.components.map((comp) => {
const className = cc.js.getClassName(comp.constructor);
return {
isCustom: Service.Script.isCustomComponent(comp.constructor),
type: className,
value: comp.uuid,
extends: CCClass.getInheritanceChain(comp.constructor)
.map((itemCtor: any) => cc.js.getClassName(itemCtor))
.filter(Boolean),
};
}),
};
};

let node: Node | null = root;
if (params.path) {
node = NodeMgr.getNodeByPath(params.path);
}
if (!node) {
return null;
}
return step(node);
} catch (error) {
console.error(error);
throw error;
} finally {
Service.Editor.unlock();
}
}

/**
* 确保节点有 UITransform 组件
* 目前只需保障在创建空节点的时候检查任意上级是否为 canvas
Expand Down
8 changes: 1 addition & 7 deletions src/core/scene/scene-process/service/prefab/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Component, editorExtrasTag, instantiate, Node, Prefab, CCClass, Scene, isValid } from 'cc';
import { isEditorNode } from '../node/node-utils';
import { ServiceEvents, Service } from '../core';
import { INodeEvents, NodeEventType } from '../../../common';
import { INodeEvents, NodeEventType, PrefabState } from '../../../common';

type PrefabInfo = Prefab._utils.PrefabInfo;
const PrefabInfo = Prefab._utils.PrefabInfo;
Expand All @@ -22,12 +22,6 @@ const TargetOverrideInfo = Prefab._utils.TargetOverrideInfo;
type MountedComponentsInfo = Prefab._utils.MountedComponentsInfo;
const MountedComponentsInfo = Prefab._utils.MountedComponentsInfo;

export enum PrefabState {
NotAPrefab = 0, // 普通节点,非Prefab
PrefabChild = 1, // Prefab子节点,不含有PrefabInstance
PrefabInstance = 2, // Prefab的根节点含有PrefabInstance的节点
PrefabLostAsset = 3, // 丢失资源的Prefab节点
}

const compKey = '_components';
const DELIMETER = CCClass.Attr.DELIMETER;
Expand Down
3 changes: 1 addition & 2 deletions src/core/scene/scene-process/service/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Rpc } from '../rpc';
import { BaseService, register } from './core';
import { IScriptEvents, IScriptService } from '../../common';
import utils from '../../../base/utils';
import { serviceManager } from './service-manager';

/**
* 异步迭代。有以下特点:
Expand Down Expand Up @@ -215,7 +214,7 @@ export class ScriptService extends BaseService<IScriptEvents> implements IScript
* 是否是自定义脚本(不是引擎定义的组件)
* @param classConstructor
*/
public async isCustomComponent(classConstructor: Function) {
public isCustomComponent(classConstructor: Function) {
return this.customComponents.has(classConstructor);
}

Expand Down
89 changes: 89 additions & 0 deletions src/core/scene/test/node-proxy.testcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type ICreateByNodeTypeParams,
type IDeleteNodeParams,
type IQueryNodeParams,
type IQueryNodeTreeParams,
type IUpdateNodeParams,
type INode,
NodeType,
Expand Down Expand Up @@ -387,4 +388,92 @@ describe('Node Proxy 测试', () => {

});
});

describe('7. queryNodeTree - 查询节点树', () => {
it('queryNodeTree - 查询整棵场景树', async () => {
const params: IQueryNodeTreeParams = {};
const tree = await NodeProxy.queryNodeTree(params);
expect(tree).toBeDefined();
expect(tree).not.toBeNull();
expect(tree!.isScene).toBe(true);
expect(tree!.name).toBeDefined();
expect(Array.isArray(tree!.children)).toBe(true);
expect(Array.isArray(tree!.components)).toBe(true);
});

it('queryNodeTree - 返回的节点包含必要字段', async () => {
const tree = await NodeProxy.queryNodeTree({});
expect(tree).not.toBeNull();

const checkFields = (item: typeof tree) => {
if (!item) return;
expect(typeof item.name).toBe('string');
expect(typeof item.active).toBe('boolean');
expect(typeof item.locked).toBe('boolean');
expect(typeof item.type).toBe('string');
expect(typeof item.path).toBe('string');
expect(typeof item.isScene).toBe('boolean');
expect(typeof item.readonly).toBe('boolean');
expect(typeof item.parent).toBe('string');
expect(item.prefab).toBeDefined();
expect(Array.isArray(item.children)).toBe(true);
expect(Array.isArray(item.components)).toBe(true);
};
checkFields(tree);
if (tree!.children.length > 0) {
checkFields(tree!.children[0]);
}
});

it('queryNodeTree - 通过 path 查询子树', async () => {
// 先创建一个节点用于查询
const createParams: ICreateByNodeTypeParams = {
path: '/',
name: 'TreeTestNode',
nodeType: NodeType.EMPTY,
};
const created = await NodeProxy.createNodeByType(createParams);
expect(created).toBeDefined();

const params: IQueryNodeTreeParams = { path: created!.path };
const subtree = await NodeProxy.queryNodeTree(params);
expect(subtree).not.toBeNull();
expect(subtree!.name).toBe('TreeTestNode');
expect(subtree!.isScene).toBe(false);

// 清理
await NodeProxy.deleteNode({ path: created!.path, keepWorldTransform: false });
});

it('queryNodeTree - 查询不存在的路径应返回 null', async () => {
const params: IQueryNodeTreeParams = { path: '/NonExistentTreeNode' };
const result = await NodeProxy.queryNodeTree(params);
expect(result).toBeNull();
});

it('queryNodeTree - 组件信息包含 type 和 extends', async () => {
// 创建一个带组件的节点
const createParams: ICreateByNodeTypeParams = {
path: '/',
name: 'CompTreeTestNode',
nodeType: NodeType.SPRITE,
};
const created = await NodeProxy.createNodeByType(createParams);
expect(created).toBeDefined();

const tree = await NodeProxy.queryNodeTree({ path: created!.path });
expect(tree).not.toBeNull();
expect(tree!.components.length).toBeGreaterThan(0);

for (const comp of tree!.components) {
expect(typeof comp.type).toBe('string');
expect(typeof comp.isCustom).toBe('boolean');
expect(typeof comp.value).toBe('string');
expect(Array.isArray(comp.extends)).toBe(true);
}

// 清理
await NodeProxy.deleteNode({ path: created!.path, keepWorldTransform: false });
});
});
});
20 changes: 20 additions & 0 deletions workflow/build-scene-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,26 @@ async function buildSceneBundle() {
return { code: fixed, map: null };
}
},
{
// Alias EditorExtends (capital) to the bundled editorExtends module.
// Service files reference EditorExtends as a bare global which resolves
// to window.EditorExtends (the stub from editor-stub-preload.js).
// This injects a var assignment right after the editor-extends IIFE
// so all subsequent references use the real module.
name: 'alias-editor-extends-global',
renderChunk(code) {
const marker = '} (editorExtends));';
if (!code.includes(marker)) {
console.warn('[Build] Warning: could not find editorExtends IIFE marker. EditorExtends global aliasing skipped.');
return null;
}
const fixed = code.replace(
marker,
marker + '\n\t\t\tvar EditorExtends = editorExtends;'
);
return { code: fixed, map: null };
}
},
nodeResolve({
preferBuiltins: true,
browser: true,
Expand Down
Loading