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
1 change: 0 additions & 1 deletion src/core/scene/common/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type { IUndoEvents } from './undo';
*/
export interface ISceneEvents {
'scene:dimension-changed': [is2D: boolean];
'scene:change-node': [...args: any[]];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class TransformBaseGizmo extends GizmoBase<Component> {
// 发送节点修改消息
protected broadcastNodeChangeMessage(node: Node) {
const EditorExtends = (cc as any).EditorExtends || (globalThis as any).EditorExtends;
ServiceEvents.broadcast('scene:change-node', EditorExtends.Node.getNodePath(node));
ServiceEvents.broadcast('node:change', EditorExtends.Node.getNodePath(node));
}

getSnappedValue(inNumber: number, snapStep: number): number {
Expand Down
6 changes: 3 additions & 3 deletions src/core/scene/scene-process/service/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ class MessageManager {
}
}

// 因为ChangeNode消息有可能每帧都发送(特别是骨骼动画),太频繁了造成卡顿,所以限制了发送频率
public broadcastChangeNodeMsg(...args: any[]) {
this._timerUtil.callFunctionLimit(args[0], this.broadcast.bind(this), 'scene:change-node', ...args);
// node:change 消息可能每帧都发送(特别是骨骼动画),太频繁造成卡顿,限制发送频率
public broadcastNodeChangeMsg(...args: any[]) {
this._timerUtil.callFunctionLimit(args[0], this.broadcast.bind(this), 'node:change', ...args);
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/core/scene/scene-process/service/prefab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ export class PrefabService extends BaseService<IPrefabEvents> implements IPrefab
if (!prefabUtils.isPrefabInstanceRoot(node) && prefabUtils.isPartOfAssetInPrefabInstance(node)) {
console.warn(`Node [${node.name}] is a prefab child of prefabInstance [${node['_prefab']?.root?.name}], ${operationTips}`);
// 消除其它面板的等待操作,例如hierarchy操作节点时会先进入等待状态,如果没有node的change消息,就会一直处于等待状态。
ServiceEvents.broadcast('scene:change-node', EditorExtends.Node.getNodePath(node));
ServiceEvents.broadcast('node:change', EditorExtends.Node.getNodePath(node));
continue;
}

Expand All @@ -506,7 +506,7 @@ export class PrefabService extends BaseService<IPrefabEvents> implements IPrefab
if (prefabUtils.isPartOfAssetInPrefabInstance(node)) {
console.warn(`Node [${node.name}] is part of prefabInstance [${node['_prefab']?.root?.name}], ${operationTips}`);
// 消除其它面板的等待操作,例如hierarchy操作节点时会先进入等待状态,如果没有node的change消息,就会一直处于等待状态。
ServiceEvents.broadcast('scene:change-node', EditorExtends.Node.getNodePath(node));
ServiceEvents.broadcast('node:change', EditorExtends.Node.getNodePath(node));
continue;
}

Expand Down Expand Up @@ -559,7 +559,7 @@ export class PrefabService extends BaseService<IPrefabEvents> implements IPrefab
console.warn(`Node [${child.name}] is a prefab child of prefabInstance [${child['_prefab'].root?.name}], \
it's not allowed to modify hierarchy in current context, you can modify it in it's prefabAsset or do it after unlink prefab from root node`);
// 消除其它面板的等待操作,例如hierarchy操作节点时会先进入等待状态,如果没有node的change消息,就会一直处于等待状态。
ServiceEvents.broadcast('scene:change-node', EditorExtends.Node.getNodePath(child));
ServiceEvents.broadcast('node:change', EditorExtends.Node.getNodePath(child));
return false;
}
}
Expand Down
3 changes: 1 addition & 2 deletions src/core/scene/scene-process/service/service-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const MESSAGE_ONLY_EVENTS = [
'gizmo:view-mode-changed',
'gizmo:tool-changed',
'scene:dimension-changed',
'scene:change-node',
'camera:mode-change',
'camera:projection-changed',
'camera:fov-changed',
Expand Down Expand Up @@ -175,7 +174,7 @@ export class ServiceManager {
}
if (!broadcastToMessage) return;
if (isNodeChange) {
messageManager.broadcastChangeNodeMsg(...args);
messageManager.broadcastNodeChangeMsg(...args);
} else {
messageManager.broadcast(eventType, ...args);
}
Expand Down
20 changes: 10 additions & 10 deletions src/core/scene/test/service-core/message-callsite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,9 +465,9 @@ describe('ServiceEvents 事件发射集成测试', () => {
NodeMock.getNodePath = jest.fn((node: any) => `/${node.name}`);
});

it('filterChildOfAssetOfPrefabInstance 中 prefab 子节点应 emit scene:change-node', () => {
it('filterChildOfAssetOfPrefabInstance 中 prefab 子节点应 emit node:change', () => {
const listener = jest.fn();
globalEventEmitter.on('scene:change-node', listener);
globalEventEmitter.on('node:change', listener);

prefabUtilsMock.isOutmostPrefabInstanceMountedChildren.mockReturnValue(false);
prefabUtilsMock.isPrefabInstanceRoot.mockReturnValue(false);
Expand All @@ -478,9 +478,9 @@ describe('ServiceEvents 事件发射集成测试', () => {
expect(listener).toHaveBeenCalledWith('/Node-child-uuid-1');
});

it('filterChildOfAssetOfPrefabInstance 中非 prefab 子节点不应 emit scene:change-node', () => {
it('filterChildOfAssetOfPrefabInstance 中非 prefab 子节点不应 emit node:change', () => {
const listener = jest.fn();
globalEventEmitter.on('scene:change-node', listener);
globalEventEmitter.on('node:change', listener);

prefabUtilsMock.isOutmostPrefabInstanceMountedChildren.mockReturnValue(false);
prefabUtilsMock.isPrefabInstanceRoot.mockReturnValue(false);
Expand All @@ -492,9 +492,9 @@ describe('ServiceEvents 事件发射集成测试', () => {
expect(result).toContain('normal-uuid');
});

it('filterPartOfPrefabAsset 中 prefab 部件应 emit scene:change-node', () => {
it('filterPartOfPrefabAsset 中 prefab 部件应 emit node:change', () => {
const listener = jest.fn();
globalEventEmitter.on('scene:change-node', listener);
globalEventEmitter.on('node:change', listener);

prefabUtilsMock.isPartOfAssetInPrefabInstance.mockReturnValue(true);

Expand All @@ -503,9 +503,9 @@ describe('ServiceEvents 事件发射集成测试', () => {
expect(listener).toHaveBeenCalledWith('/Node-part-uuid');
});

it('filterPartOfPrefabAsset 中非 prefab 部件不应 emit scene:change-node', () => {
it('filterPartOfPrefabAsset 中非 prefab 部件不应 emit node:change', () => {
const listener = jest.fn();
globalEventEmitter.on('scene:change-node', listener);
globalEventEmitter.on('node:change', listener);

prefabUtilsMock.isPartOfAssetInPrefabInstance.mockReturnValue(false);

Expand All @@ -515,9 +515,9 @@ describe('ServiceEvents 事件发射集成测试', () => {
expect(result).toContain('normal-uuid');
});

it('canModifySibling 中不可移动的 prefab 子节点应 emit scene:change-node', () => {
it('canModifySibling 中不可移动的 prefab 子节点应 emit node:change', () => {
const listener = jest.fn();
globalEventEmitter.on('scene:change-node', listener);
globalEventEmitter.on('node:change', listener);

const child = {
uuid: 'prefab-child',
Expand Down
62 changes: 31 additions & 31 deletions src/core/scene/test/service-core/message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,36 +106,36 @@ describe('MessageManager', () => {
});
});

// ── broadcastChangeNodeMsg 节流功能 ──
// ── broadcastNodeChangeMsg 节流功能 ──

describe('broadcastChangeNodeMsg', () => {
it('首次调用应立即广播 scene:change-node', () => {
describe('broadcastNodeChangeMsg', () => {
it('首次调用应立即广播 node:change', () => {
const listener = jest.fn();
messageManager.on('scene:change-node', listener);
messageManager.on('node:change', listener);

messageManager.broadcastChangeNodeMsg('uuid-1');
messageManager.broadcastNodeChangeMsg('uuid-1');

expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith('uuid-1');
});

it('200ms 内同 uuid 重复调用应被节流', () => {
const listener = jest.fn();
messageManager.on('scene:change-node', listener);
messageManager.on('node:change', listener);

messageManager.broadcastChangeNodeMsg('uuid-1');
messageManager.broadcastChangeNodeMsg('uuid-1');
messageManager.broadcastChangeNodeMsg('uuid-1');
messageManager.broadcastNodeChangeMsg('uuid-1');
messageManager.broadcastNodeChangeMsg('uuid-1');
messageManager.broadcastNodeChangeMsg('uuid-1');

expect(listener).toHaveBeenCalledTimes(1);
});

it('200ms 后应执行最后一次挂起的调用', () => {
const listener = jest.fn();
messageManager.on('scene:change-node', listener);
messageManager.on('node:change', listener);

messageManager.broadcastChangeNodeMsg('uuid-1');
messageManager.broadcastChangeNodeMsg('uuid-1');
messageManager.broadcastNodeChangeMsg('uuid-1');
messageManager.broadcastNodeChangeMsg('uuid-1');

jest.advanceTimersByTime(200);

Expand All @@ -146,19 +146,19 @@ describe('MessageManager', () => {

it('不同 uuid 的节点应各自独立广播', () => {
const listener = jest.fn();
messageManager.on('scene:change-node', listener);
messageManager.on('node:change', listener);

messageManager.broadcastChangeNodeMsg('uuid-A');
messageManager.broadcastChangeNodeMsg('uuid-B');
messageManager.broadcastNodeChangeMsg('uuid-A');
messageManager.broadcastNodeChangeMsg('uuid-B');

expect(listener).toHaveBeenCalledTimes(2);
expect(listener).toHaveBeenNthCalledWith(1, 'uuid-A');
expect(listener).toHaveBeenNthCalledWith(2, 'uuid-B');
});

it('节流期间 process.send 不应被调用', () => {
messageManager.broadcastChangeNodeMsg('uuid-1');
messageManager.broadcastChangeNodeMsg('uuid-1');
messageManager.broadcastNodeChangeMsg('uuid-1');
messageManager.broadcastNodeChangeMsg('uuid-1');

expect(processSend).not.toHaveBeenCalled();

Expand Down Expand Up @@ -201,12 +201,12 @@ describe('MessageManager', () => {
expect(listener).toHaveBeenCalledWith(mockNode);
});

it('scene:change-node — 通过 broadcastChangeNodeMsg 节流广播', () => {
it('node:change — 通过 broadcastNodeChangeMsg 节流广播', () => {
const listener = jest.fn();
messageManager.on('scene:change-node', listener);
messageManager.on('node:change', listener);

messageManager.broadcastChangeNodeMsg('node-uuid-1');
messageManager.broadcastChangeNodeMsg('node-uuid-1');
messageManager.broadcastNodeChangeMsg('node-uuid-1');
messageManager.broadcastNodeChangeMsg('node-uuid-1');

expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith('node-uuid-1');
Expand Down Expand Up @@ -385,13 +385,13 @@ describe('MessageManager', () => {

// ── 骨骼动画高频场景 ──

describe('高频 change-node 场景(骨骼动画模拟)', () => {
it('连续 10 次 broadcastChangeNodeMsg 同一节点,只应执行 2 次', () => {
describe('高频 node-change 场景(骨骼动画模拟)', () => {
it('连续 10 次 broadcastNodeChangeMsg 同一节点,只应执行 2 次', () => {
const listener = jest.fn();
messageManager.on('scene:change-node', listener);
messageManager.on('node:change', listener);

for (let i = 0; i < 10; i++) {
messageManager.broadcastChangeNodeMsg('bone-uuid');
messageManager.broadcastNodeChangeMsg('bone-uuid');
}

expect(listener).toHaveBeenCalledTimes(1);
Expand All @@ -403,13 +403,13 @@ describe('MessageManager', () => {

it('多个骨骼节点并发变更应各自独立节流', () => {
const listener = jest.fn();
messageManager.on('scene:change-node', listener);
messageManager.on('node:change', listener);

messageManager.broadcastChangeNodeMsg('bone-1');
messageManager.broadcastChangeNodeMsg('bone-2');
messageManager.broadcastChangeNodeMsg('bone-3');
messageManager.broadcastChangeNodeMsg('bone-1');
messageManager.broadcastChangeNodeMsg('bone-2');
messageManager.broadcastNodeChangeMsg('bone-1');
messageManager.broadcastNodeChangeMsg('bone-2');
messageManager.broadcastNodeChangeMsg('bone-3');
messageManager.broadcastNodeChangeMsg('bone-1');
messageManager.broadcastNodeChangeMsg('bone-2');

// bone-1, bone-2, bone-3 各首次立即执行 = 3
expect(listener).toHaveBeenCalledTimes(3);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* ServiceEvents → MessageManager 事件名一致性测试
*
* 确保 ServiceManager 转发事件时,ServiceEvents 上的事件名
* 与 MessageManager 广播的事件名完全一致,不发生名称翻译。
*/

jest.mock('cc', () => ({}));

jest.mock('../../scene-process/service/core', () => {
const actual = jest.requireActual('../../scene-process/service/core/global-events');
return {
getServiceAll: () => [],
ServiceEvents: actual.ServiceEvents,
};
});

jest.mock('../../scene-process/service/core/internal-events', () => ({
InternalServiceEvents: {
EditorReloadClose: 'editor:reload-close',
EditorReloadOpen: 'editor:reload-open',
EditorDisposed: 'editor:disposed',
},
}));

import { globalEventEmitter } from '../../scene-process/service/core/global-events';
import { messageManager } from '../../scene-process/service/message';
import { serviceManager } from '../../scene-process/service/service-manager';

// ── ServiceManager 转发的所有事件 ──

const SERVICE_MAP_EVENTS = [
'editor:open', 'editor:close', 'editor:reload', 'editor:save',
'node:add', 'node:remove', 'node:before-remove', 'node:before-add',
'node:before-change', 'node:change', 'node:added', 'node:removed',
'asset:change', 'asset:deleted',
'component:add', 'component:remove', 'component:added', 'component:removed',
'component:set-property', 'component:before-add-component', 'component:before-remove-component',
'script:execution-finished',
'selection:select', 'selection:unselect', 'selection:clear',
];

const MESSAGE_ONLY_EVENTS = [
'dirty:changed',
'gizmo:coordinate-changed', 'gizmo:pivot-changed', 'gizmo:view-mode-changed', 'gizmo:tool-changed',
'scene:dimension-changed',
'camera:mode-change', 'camera:projection-changed', 'camera:fov-changed',
'scene-view:visibility-changed', 'scene-view:light-changed',
];

const ALL_FORWARDED_EVENTS = [...SERVICE_MAP_EVENTS, ...MESSAGE_ONLY_EVENTS];

// ── 测试 ──

describe('ServiceEvents → MessageManager 事件名一致性', () => {
beforeAll(() => {
serviceManager.initialize('http://test');
});

beforeEach(() => {
jest.useFakeTimers();
messageManager.clear();
(messageManager as any)._timerUtil.clear();
});

afterEach(() => {
jest.useRealTimers();
});

it.each(ALL_FORWARDED_EVENTS)(
'"%s": ServiceEvents 与 MessageManager 广播名一致',
(eventName) => {
const listener = jest.fn();
messageManager.on(eventName, listener);

globalEventEmitter.emit(eventName, 'test-arg');

expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith('test-arg');
}
);

it('内部事件不应转发到 MessageManager', () => {
const listener = jest.fn();
messageManager.on('editor:reload-close', listener);
messageManager.on('editor:reload-open', listener);
messageManager.on('editor:disposed', listener);

globalEventEmitter.emit('editor:reload-close');
globalEventEmitter.emit('editor:reload-open');
globalEventEmitter.emit('editor:disposed');

expect(listener).not.toHaveBeenCalled();
});
});
Loading