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 62bc63e93..c1d8702b6 100644 --- a/src/core/scene/scene-process/service/editors/prefab-editor.ts +++ b/src/core/scene/scene-process/service/editors/prefab-editor.ts @@ -33,6 +33,7 @@ export class PrefabEditor extends BaseEditor { // 实例化预制体 const instance = instantiate(prefabAsset); + editorPrefabUtils.preparePrefabRootForEditing(instance); this.virtualScene.addChild(instance); await this.ensurePreviewCanvasForUI(instance); @@ -84,6 +85,7 @@ export class PrefabEditor extends BaseEditor { if (!instance) { throw new Error(`reload 失败,找不到预制体根节点: ${prefabName}`); } + editorPrefabUtils.preparePrefabRootForEditing(instance); this.entity.instance = instance; Prefab._utils.applyTargetOverrides(this.entity.instance); await this.ensurePreviewCanvasForUI(this.entity.instance); diff --git a/src/core/scene/scene-process/service/prefab/prefab-editor-utils.ts b/src/core/scene/scene-process/service/prefab/prefab-editor-utils.ts index c45fd1585..afa579694 100644 --- a/src/core/scene/scene-process/service/prefab/prefab-editor-utils.ts +++ b/src/core/scene/scene-process/service/prefab/prefab-editor-utils.ts @@ -4,6 +4,13 @@ import { prefabUtils } from './utils'; type UUIDMap = Map; class EditorPrefabUtils { + preparePrefabRootForEditing(node: Node) { + const prefabInfo = node['_prefab']; + if (prefabInfo) { + prefabInfo.instance = undefined; + } + } + serialize(node: Node) { // 校验数据 prefabUtils.checkMountedRootData(node, true); diff --git a/src/core/scene/test/prefab-editor-preview-canvas.test.ts b/src/core/scene/test/prefab-editor-preview-canvas.test.ts index 173963d51..368547d16 100644 --- a/src/core/scene/test/prefab-editor-preview-canvas.test.ts +++ b/src/core/scene/test/prefab-editor-preview-canvas.test.ts @@ -49,42 +49,65 @@ jest.mock('../scene-process/service/prefab/prefab-editor-utils', () => ({ restorePrefabUUID: jest.fn(), generateSceneAsset: jest.fn(), removePrefabInstanceRoots: jest.fn(), + preparePrefabRootForEditing: jest.fn((node: any) => { + if (node?._prefab) { + node._prefab.instance = undefined; + } + }), }, })); -import { instantiate } from 'cc'; +import { find, instantiate } from 'cc'; import { sceneUtils } from '../scene-process/service/scene/utils'; import { PrefabEditor } from '../scene-process/service/editors/prefab-editor'; -async function openPrefabWith(prefabRoot: any, scene = new MockScene('virtual-scene')): Promise { +function createPrefabRoot(name: string, options?: { hasCanvas?: boolean; hasUI?: boolean; nestedChild?: any }) { + return { + name, + uuid: `${name}-uuid`, + parent: null, + children: options?.nestedChild ? [options.nestedChild] : [], + _prefab: { fileId: `${name}-file-id`, instance: { fileId: `${name}-instance-file-id` } }, + getComponentInChildren: jest.fn((type: unknown) => type === MockCanvas && options?.hasCanvas ? {} : null), + getComponentsInChildren: jest.fn((type: unknown) => type === MockUITransform && options?.hasUI !== false ? [{}] : []), + }; +} + +async function openPrefabWith(prefabRoot: any, scene = new MockScene('virtual-scene')): Promise<{ editor: PrefabEditor; scene: MockScene; }> { (sceneUtils.runScene as jest.Mock).mockResolvedValue(scene); (sceneUtils.loadAny as jest.Mock).mockResolvedValue({}); (instantiate as unknown as jest.Mock).mockReturnValue(prefabRoot); + const editor = new PrefabEditor(); - await new PrefabEditor().open({ + await editor.open({ uuid: 'prefab-uuid', name: 'LabelPrefab', type: 'prefab', url: 'db://assets/LabelPrefab.prefab', } as never); - return scene; + return { editor, scene }; } describe('PrefabEditor preview Canvas', () => { beforeEach(() => { jest.clearAllMocks(); + (globalThis as any).EditorExtends = { + serialize: jest.fn(() => ({ json: 'scene-asset' })), + Node: { + getNode: jest.fn(), + }, + }; + }); + + afterEach(() => { + delete (globalThis as any).EditorExtends; }); it('hosts a UI prefab without its own Canvas under an editor-only Canvas when opened', async () => { const scene = new MockScene('virtual-scene'); const previewCanvas = { name: 'should_hide_in_hierarchy' }; - const prefabRoot = { - name: 'LabelPrefab', - parent: null, - getComponentInChildren: jest.fn((type: unknown) => type === MockCanvas ? null : null), - getComponentsInChildren: jest.fn((type: unknown) => type === MockUITransform ? [{}] : []), - }; + const prefabRoot = createPrefabRoot('LabelPrefab'); createShouldHideInHierarchyCanvasNode.mockResolvedValue(previewCanvas); @@ -95,30 +118,48 @@ describe('PrefabEditor preview Canvas', () => { }); it('does not create a preview Canvas when the prefab already owns one', async () => { - const prefabRoot = { - name: 'CanvasPrefab', - parent: null, - getComponentInChildren: jest.fn((type: unknown) => type === MockCanvas ? {} : null), - getComponentsInChildren: jest.fn((type: unknown) => type === MockUITransform ? [{}] : []), - }; + const prefabRoot = createPrefabRoot('CanvasPrefab', { hasCanvas: true }); - const scene = await openPrefabWith(prefabRoot); + const { scene } = await openPrefabWith(prefabRoot); expect(createShouldHideInHierarchyCanvasNode).not.toHaveBeenCalled(); expect(prefabRoot.parent).toBe(scene); }); it('does not create a preview Canvas for prefabs without UI components', async () => { - const prefabRoot = { - name: 'MeshPrefab', - parent: null, - getComponentInChildren: jest.fn(() => null), - getComponentsInChildren: jest.fn(() => []), - }; + const prefabRoot = createPrefabRoot('MeshPrefab', { hasUI: false }); - const scene = await openPrefabWith(prefabRoot); + const { scene } = await openPrefabWith(prefabRoot); expect(createShouldHideInHierarchyCanvasNode).not.toHaveBeenCalled(); expect(prefabRoot.parent).toBe(scene); }); + + it('clears root prefab instance but keeps nested child instance when opened', async () => { + const nestedChild = createPrefabRoot('NestedChild'); + const prefabRoot = createPrefabRoot('RootPrefab', { nestedChild }); + + await openPrefabWith(prefabRoot); + + expect(prefabRoot._prefab.instance).toBeUndefined(); + expect(nestedChild._prefab.instance).toEqual({ fileId: 'NestedChild-instance-file-id' }); + }); + + it('clears reloaded root prefab instance but keeps nested child instance after reload', async () => { + const nestedChild = createPrefabRoot('NestedChild'); + const openedRoot = createPrefabRoot('RootPrefab', { nestedChild }); + const { editor } = await openPrefabWith(openedRoot); + const reloadedNestedChild = createPrefabRoot('ReloadedNestedChild'); + const reloadedRoot = createPrefabRoot('RootPrefab', { nestedChild: reloadedNestedChild }); + const reloadedScene = new MockScene('reloaded-scene'); + + (sceneUtils.runSceneImmediateByJson as jest.Mock).mockResolvedValue(reloadedScene); + ((globalThis as any).EditorExtends.Node.getNode as jest.Mock).mockReturnValue(reloadedRoot); + (find as unknown as jest.Mock).mockReturnValue(null); + + await editor.reload(); + + expect(reloadedRoot._prefab.instance).toBeUndefined(); + expect(reloadedNestedChild._prefab.instance).toEqual({ fileId: 'ReloadedNestedChild-instance-file-id' }); + }); });