'use strict';
const fs = require('fs');
const path = require('path');
module.paths.push(path.join(Editor.App.path, 'node_modules'));
const { clipboard } = require('electron');
const Profile = require('@base/electron-profile');
const { throttle } = require('lodash');
const utils = require('./utils');
const { trackEventWithTimer } = require('../utils/metrics');
const { injectionStyle, setLabel } = require('../utils/prop');
// ipc messages protocol
const messageProtocol = {
scene: 'scene',
};
const lockList = [];
let lockPerform = false;
let lastSnapShot = null;
async function performLock() {
if (lockPerform) { return; }
if (lockList.length === 0) { return; }
lockPerform = true;
const params = lockList.shift();
const { snapshotLock, lock, uuids, cancel } = params;
if (snapshotLock && !lock) {
if (lastSnapShot) {
await endRecording(lastSnapShot, cancel);
lastSnapShot = null;
}
// start snapshot
} else if (!snapshotLock && lock) {
lastSnapShot = await beginRecording(uuids);
}
lockPerform = false;
await performLock();
}
/**
* 替换之前的snapshotLock,由于UI层的事件是同步的,
* 而新的beginRecording是异步的,所以需要使用队列来保证顺序
* @param {*} lock
* @param {*} uuids
* @param {*} cancel
*/
function snapshotLock(panel, lock, uuids, cancel = false) {
// 保存当前状态,放到队列中
const params = {
snapshotLock:panel.snapshotLock,
lock,
uuids,
cancel,
};
lockList.push(params);
// 执行队列中的操作;
performLock();
panel.snapshotLock = lock;
}
// 不传options时,会自动记录到undo队列,不需要调用endRecording
async function beginRecording(uuids, options) {
if (!uuids) { return; }
const undoID = await Editor.Message.request(messageProtocol.scene, 'begin-recording', uuids, options);
return undoID;
}
async function endRecording(undoID, cancel) {
if (!undoID) { return; }
if (cancel) {
await Editor.Message.request(messageProtocol.scene, 'cancel-recording', undoID);
} else {
await Editor.Message.request(messageProtocol.scene, 'end-recording', undoID);
}
}
exports.listeners = {
async 'change-dump'(event) {
const panel = this;
const target = event.target;
if (!target) {
return;
}
clearTimeout(panel.previewTimeId);
if (!panel.snapshotLock) {
snapshotLock(panel, true, panel.uuidList);
}
const dump = target.dump;
if (!dump || panel.isDialoging) {
return;
}
let setChildrenLayer = false;
if (dump.path === 'layer') {
if (panel.dumps && panel.dumps.some((perdump) => perdump.children && perdump.children.length > 0)) {
// 只修改自身节点
let choose = 1;
// 有子节点的时候才弹出对话框
panel.isDialoging = true;
const warnResult = await Editor.Dialog.warn(Editor.I18n.t(`ENGINE.components.layer.confirm_message`), {
buttons: [
Editor.I18n.t('ENGINE.components.layer.change_children'),
Editor.I18n.t('ENGINE.components.layer.change_self'),
'Cancel',
],
cancel: 2,
});
choose = warnResult.response;
panel.isDialoging = false;
// 取消,需要还原数值
if (choose === 2) {
dump.value = panel.$.nodeLayerSelect.prevValues[0];
if (dump.values) {
dump.values = panel.$.nodeLayerSelect.prevValues;
}
Elements.layer.update.call(panel);
return;
} else {
setChildrenLayer = choose === 0 ? true : false;
}
}
}
try {
panel.readyToUpdate = false;
for (let i = 0; i < panel.uuidList.length; i++) {
const uuid = panel.uuidList[i];
const { path, type, isArray } = dump;
let value = dump.value;
if (dump.values) {
value = dump.values[i];
}
if (setChildrenLayer) {
await Editor.Message.request(messageProtocol.scene, 'set-node-and-children-layer', {
uuid,
dump: {
value,
},
});
continue;
}
await Editor.Message.request(messageProtocol.scene, 'set-property', {
uuid,
path,
dump: {
type,
isArray,
value,
},
});
}
} catch (error) {
console.error(error);
} finally {
if (!panel.snapshotLock) {
snapshotLock(panel, false);
}
panel.readyToUpdate = true;
}
},
'confirm-dump'() {
const panel = this;
clearTimeout(panel.previewTimeId);
snapshotLock(panel, false);
// In combination with change-dump, snapshot only generated once after ui-elements continuously changed.
},
async 'create-dump'(event) {
const panel = this;
const target = event.target;
if (!target) {
return;
}
clearTimeout(panel.previewTimeId);
const undoID = await beginRecording(panel.uuidList);
const dump = target.dump;
let cancel = false;
try {
for (let i = 0; i < panel.uuidList.length; i++) {
const uuid = panel.uuidList[i];
if (i > 0) {
dump.values[i] = dump.value;
}
await Editor.Message.request(messageProtocol.scene, 'update-property-from-null', {
uuid,
path: dump.path,
});
}
} catch (error) {
cancel = true;
console.error(error);
}
await endRecording(undoID, cancel);
},
async 'reset-dump'(event) {
const panel = this;
const target = event.target;
if (!target) {
return;
}
clearTimeout(panel.previewTimeId);
const undoID = await beginRecording(panel.uuidList);
const dump = target.dump;
try {
for (let i = 0; i < panel.uuidList.length; i++) {
const uuid = panel.uuidList[i];
if (i > 0) {
dump.values[i] = dump.value;
}
await Editor.Message.request(messageProtocol.scene, 'reset-property', {
uuid,
path: dump.path,
});
}
} catch (error) {
await endRecording(undoID, true);
console.error(error);
}
},
'preview-dump'(event) {
const panel = this;
const target = event.target;
if (!target) {
return;
}
const dump = target.dump;
if (!dump || panel.isDialoging) {
return;
}
/**
* Some assets don`t need to preview, like:
* cc.AnimationClip
*/
const notNeedToPreview = [
'cc.AnimationClip',
];
if (notNeedToPreview.includes(dump.type)) {
return;
}
const { method, value: assetUuid } = event.detail;
clearTimeout(panel.previewTimeId);
if (method === 'confirm') {
try {
panel.previewTimeId = setTimeout(() => {
for (let i = 0; i < panel.uuidList.length; i++) {
const uuid = panel.uuidList[i];
const { path, type } = dump;
let value = dump.value;
if (dump.values) {
value = dump.values[i];
}
// 预览新的值
value.uuid = assetUuid;
Editor.Message.send(messageProtocol.scene, 'preview-set-property', {
uuid,
path,
dump: {
type,
value,
},
});
}
}, 500);
} catch (error) {
console.error(error);
}
} else if (method === 'cancel') {
panel.previewTimeId = setTimeout(() => {
try {
for (let i = 0; i < panel.uuidList.length; i++) {
const uuid = panel.uuidList[i];
const { path } = dump;
Editor.Message.send(messageProtocol.scene, 'cancel-preview-set-property', {
uuid,
path,
});
}
} catch (error) {
console.error(error);
}
}, 50);
}
},
};
exports.template = /* html*/`
Reflection Convolution
Bake
Remove
Edit
`;
exports.style = fs.readFileSync(path.join(__dirname, './node.css'), 'utf8');
exports.$ = {
container: '.container',
header: '.container > .header',
body: '.container > .body',
prefab: '.container > .header > .prefab',
prefabUnlink: '.container > .header > .prefab [role="unlink"]',
prefabMore: '.container > .header > .prefab [role="show-more"]',
prefabLocal: '.container > .header > .prefab > [role="local"]',
prefabReset: '.container > .header > .prefab > [role="reset"]',
prefabSave: '.container > .header > .prefab > [role="save"]',
prefabEdit: '.container > .header > .prefab > [role="edit"]',
active: '.container > .header > .node > .active',
name: '.container > .header > .node > .name',
scene: '.container > .body > .scene',
sceneRelease: '.container > .body > .scene > .release',
sceneAmbient: '.container > .body > .scene > .ambient',
sceneFog: '.container > .body > .scene > .fog',
sceneShadows: '.container > .body > .scene > .shadows',
sceneSkybox: '.container > .body > .scene > .skybox',
sceneSkyboxBefore: '.container > .body > .scene > .skybox > .before',
sceneSkyboxUseHDR: '.container > .body > .scene > .skybox .useHDR',
sceneSkyboxEnvmapHDR: '.container > .body > .scene > .skybox .envmapHDR',
sceneSkyboxEnvmapLDR: '.container > .body > .scene > .skybox .envmapLDR',
sceneSkyboxReflection: '.container > .body > .scene > .skybox .reflection',
sceneSkyboxReflectionLoading: '.container > .body > .scene > .skybox .reflection ui-loading',
sceneSkyboxReflectionBake: '.container > .body > .scene > .skybox .reflection .bake',
sceneSkyboxReflectionRemove: '.container > .body > .scene > .skybox .reflection .remove',
sceneSkyboxAfter: '.container > .body > .scene > .skybox > .after',
sceneOctree: '.container > .body > .scene > .octree',
sceneSkin: '.container > .body > .scene > .skin',
scenePostSettings: '.container > .body > .scene > .postSettings',
node: '.container > .body > .node',
nodeHeader: '.container > .body > .node > .component-header',
nodeSection: '.container > .body > .node >.node-section',
nodeMenu: '.container > .body > .node > .component-header > .menu',
nodeLink: '.container > .body > .node > .component-header > .link',
nodePosition: '.container > .body > .node > .position',
nodeRotation: '.container > .body > .node > .rotation',
nodeScale: '.container > .body > .node > .scale',
nodeMobility: '.container > .body > .node > .mobility',
nodeLayer: '.container > .body > .node > .layer > ui-label',
nodeLayerSelect: '.container > .body > .node > .layer .layer-select',
nodeLayerButton: '.container > .body > .node > .layer .layer-edit',
sectionBody: '.container > .body > .section-body',
sectionMissing: '.container > .body > .section-missing',
sectionAsset: '.container > .body > .section-asset',
footer: '.container > .body > .footer',
componentAdd: '.container > .body > .footer .add-component',
};
const Elements = {
panel: {
ready() {
const panel = this;
panel.throttleUpdate = throttle(async () => {
if (!panel.readyToUpdate) {
return;
}
for (const prop in Elements) {
const element = Elements[prop];
if (element.update) {
await element.update.call(panel);
}
}
}, 100, { leading: false, trailing: true });
panel.__nodeChanged__ = (uuid) => {
if (panel.throttleUpdate && Array.isArray(panel.uuidList) && panel.uuidList.includes(uuid)) {
panel.throttleUpdate();
}
};
Editor.Message.addBroadcastListener('scene:change-node', panel.__nodeChanged__);
panel.__animationTimeChange__ = () => {
if (!panel.isAnimationMode()) {
return;
}
panel.__nodeChanged__(panel.uuidList[0]);
};
Editor.Message.addBroadcastListener('scene:animation-time-change', panel.__animationTimeChange__);
panel.__projectSettingChanged__ = async function(name) {
if (name !== 'layers') {
return;
}
for (const prop in Elements) {
const element = Elements[prop];
if (element.update) {
await element.update.call(panel);
}
}
};
Editor.Message.addBroadcastListener('project:setting-change', panel.__projectSettingChanged__);
panel.__throttleProfileChanged__ = throttle(async (protocol, file, key) => {
if (protocol === 'defaultPreferences' && file === 'packages/inspector.json' && key === 'message-protocol') {
await panel.__queryMessageProtocolScene__();
for (const prop in Elements) {
const element = Elements[prop];
if (element.update) {
await element.update.call(panel);
}
}
}
}, 100, { leading: false, trailing: true });
if (panel.__throttleProfileChanged__) {
Profile.on('change', panel.__throttleProfileChanged__);
}
// 识别拖入脚本资源
panel.$.container.addEventListener('dragover', (event) => {
event.preventDefault();
if (panel.dump.isScene) {
event.dataTransfer.dropEffect = 'none';
} else {
event.dataTransfer.dropEffect = 'copy';
}
});
panel.$.container.addEventListener('drop', async (event) => {
event.preventDefault();
event.stopPropagation();
if (panel.dump.isScene) {
return;
}
// 支持多选脚本拖入
const { additional = [], value, type } = JSON.parse(JSON.stringify(Editor.UI.DragArea.currentDragInfo)) || {};
if (value && additional.every((v) => v.value !== value)) {
additional.push({ value, type });
}
const undoID = await Editor.Message.request('scene', 'begin-recording', panel.uuidList);
for (const info of additional) {
const config = panel.dropConfig[info.type];
if (config) {
await Editor.Message.request(config.package, config.message, info, panel.dumps, panel.uuidList);
}
}
await Editor.Message.request('scene', 'end-recording', undoID);
});
panel._readyToUpdate = true;
if (panel.readyToUpdate === undefined) {
Object.defineProperty(panel, 'readyToUpdate', {
enumerable: true,
get() {
return panel._readyToUpdate;
},
set(val) {
panel._readyToUpdate = val;
if (val && panel.throttleUpdate) {
panel.throttleUpdate();
}
},
});
}
},
async update() {
const panel = this;
let dumps = [];
try {
dumps = await Promise.all(
panel.uuidList.map((uuid) => {
return Editor.Message.request(messageProtocol.scene, 'query-node', uuid);
}),
);
} catch (err) {
console.error(err);
}
dumps = dumps.filter(Boolean);
panel.dump = dumps[0];
panel.dumps = [];
panel.uuidList = [];
panel.assets = {};
if (panel.dump) {
panel.$.container.removeAttribute('hidden');
// 以第一个节点的类型,过滤多选的其他不同类型,比如 node 和 sceneNode 就不能混为多选编辑
const type = panel.dump.__type__;
dumps.forEach((dump) => {
if (dump.__type__ === type) {
panel.uuidList.push(dump.uuid.value);
panel.dumps.push(dump);
}
});
// 补充缺失的 dump 数据,如 path values 等,收集节点内的资源
utils.translationDump(panel.dump, panel.dumps.length > 1 ? panel.dumps : undefined, panel.assets);
} else {
panel.$.container.setAttribute('hidden', '');
}
},
close() {
const panel = this;
if (panel.throttleUpdate) {
panel.throttleUpdate.cancel();
}
panel.throttleUpdate = undefined;
Editor.Message.removeBroadcastListener('scene:change-node', panel.__nodeChanged__);
Editor.Message.removeBroadcastListener('scene:animation-time-change', panel.__animationTimeChange__);
Editor.Message.removeBroadcastListener('project:setting-change', panel.__projectSettingChanged__);
if (panel.__throttleProfileChanged__) {
Profile.removeListener('change', panel.__throttleProfileChanged__);
panel.__throttleProfileChanged__.cancel();
}
panel.__throttleProfileChanged__ = undefined;
},
},
prefab: {
ready() {
const panel = this;
panel.$.prefab.addEventListener('confirm', async (event) => {
const button = event.target;
if (!panel.dump || !panel.dump.__prefab__ || !button || !panel.dumps?.length) {
return;
}
const role = button.getAttribute('role');
for (const dump of panel.dumps) {
const prefab = dump.__prefab__;
switch (role) {
case 'edit': {
const assetId = prefab.prefabStateInfo?.assetUuid;
if (!assetId) {
return;
}
Editor.Message.request('asset-db', 'open-asset', assetId);
break;
}
case 'unlink': {
await Editor.Message.request(messageProtocol.scene, 'unlink-prefab', prefab.rootUuid, false);
break;
}
case 'local': {
Editor.Message.send('assets', 'twinkle', prefab.uuid);
break;
}
case 'reset': {
await Editor.Message.request(messageProtocol.scene, 'restore-prefab', prefab.rootUuid, prefab.uuid);
break;
}
case 'save': {
// apply-prefab是自定义的undo,在场景中实现了undo
await Editor.Message.request(messageProtocol.scene, 'apply-prefab', prefab.rootUuid);
break;
}
case 'show-more': {
Editor.Menu.popup({
menu: [
{
label: Editor.I18n.t('ENGINE.prefab.unlink'),
async click() {
await Editor.Message.request(messageProtocol.scene, 'unlink-prefab', prefab.rootUuid, false);
},
},
{
label: Editor.I18n.t('ENGINE.prefab.unlink_recursively'),
async click() {
await Editor.Message.send('hierarchy', 'unlink-prefab-recursively');
},
},
],
});
break;
}
}
}
});
},
async update() {
const panel = this;
if (!panel.dump || !panel.dump.__prefab__) {
panel.$.prefab.setAttribute('hidden', '');
return;
}
panel.$.prefab.removeAttribute('hidden');
const prefab = panel.dump.__prefab__;
const prefabStateInfo = prefab.prefabStateInfo;
const canUnlink = panel.dumps.some(dump => {
if (dump.__prefab__ && dump.__prefab__.prefabStateInfo) {
const state = dump.__prefab__.prefabStateInfo.state;
if (state === 2 || state === 3) {
return true;
}
}
return false;
});
if (canUnlink) {
panel.$.prefabUnlink.removeAttribute('disabled');
panel.$.prefabMore.removeAttribute('disabled');
} else {
panel.$.prefabUnlink.setAttribute('disabled', '');
panel.$.prefabMore.setAttribute('disabled', '');
}
const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', prefab.uuid);
if (assetInfo) {
panel.$.prefab.removeAttribute('missing');
if (prefabStateInfo.isRevertable) {
panel.$.prefabReset.removeAttribute('disabled');
} else {
panel.$.prefabReset.setAttribute('disabled', '');
}
if (prefabStateInfo.isApplicable) {
panel.$.prefabSave.removeAttribute('disabled');
} else {
panel.$.prefabSave.setAttribute('disabled', '');
}
if (assetInfo.uuid.includes('@')) {
panel.$.prefabEdit.setAttribute('disabled', '');
} else {
panel.$.prefabEdit.removeAttribute('disabled');
}
} else {
panel.$.prefab.setAttribute('missing', '');
panel.$.prefabEdit.setAttribute('disabled', '');
panel.$.prefabLocal.setAttribute('disabled', '');
panel.$.prefabReset.setAttribute('disabled', '');
panel.$.prefabSave.setAttribute('disabled', '');
}
if ((panel.dumps && panel.dumps.length > 1) || (Editor.EditMode.getMode() === 'prefab' && !prefabStateInfo.isUnwrappable)) {
panel.$.prefabEdit.setAttribute('disabled', '');
}
},
},
header: {
ready() {
const panel = this;
panel.$.active.addEventListener('change', (event) => {
const value = event.target.value;
const dump = event.target.dump;
dump.value = value;
if ('values' in dump) {
dump.values.forEach((val, index) => {
dump.values[index] = value;
});
}
panel.$.active.dispatch('change-dump');
});
panel.$.active.addEventListener('confirm', () => {
panel.$.active.dispatch('confirm-dump');
});
panel.$.name.addEventListener('change', (event) => {
const value = event.target.value;
const dump = event.target.dump;
dump.value = value;
if ('values' in dump) {
dump.values.forEach((val, index) => {
dump.values[index] = value;
});
}
panel.$.name.dispatch('change-dump');
});
panel.$.name.addEventListener('confirm', () => {
panel.$.name.dispatch('confirm-dump');
});
},
update() {
const panel = this;
if (!panel.dump) {
return;
}
let activeDisabled = false;
let activeInvalid = false;
let nameDisabled = false;
let nameInvalid = false;
if (panel.dump.isScene) {
activeDisabled = true;
nameDisabled = true;
} else {
if (panel.dumps && panel.dumps.length > 1) {
// when changing, stop validating
if (!panel.$.active.hasAttribute('focused')) {
if (panel.dumps.some((dump) => dump.active.value !== panel.dump.active.value)) {
activeInvalid = true;
}
}
// when changing, stop validating
if (!panel.$.name.hasAttribute('focused')) {
if (panel.dumps.some((dump) => dump.name.value !== panel.dump.name.value)) {
nameInvalid = true;
}
}
}
}
panel.$.active.value = panel.dump.active.value;
panel.$.active.dump = panel.dump.active;
panel.$.active.disabled = activeDisabled;
panel.$.active.invalid = activeInvalid;
panel.$.name.value = panel.dump.name.value;
panel.$.name.dump = panel.dump.name;
panel.$.name.disabled = nameDisabled;
panel.$.name.invalid = nameInvalid;
},
},
scene: {
ready() {
const panel = this;
const $help = panel.$.sceneSkybox.querySelector('ui-link');
panel.setHelpUrl($help, { help: 'i18n:cc.Skybox' });
$help.addEventListener('click', (event) => {
event.stopPropagation();
event.preventDefault();
});
panel.$.sceneSkyboxUseHDR.addEventListener('change', Elements.scene.skyboxUseHDRChange.bind(panel));
panel.$.sceneSkyboxEnvmapHDR.addEventListener('change-dump', Elements.scene.skyboxEnvmapChange.bind(panel, true));
panel.$.sceneSkyboxEnvmapLDR.addEventListener('change-dump', Elements.scene.skyboxEnvmapChange.bind(panel, false));
panel.$.sceneSkyboxReflectionBake.addEventListener('confirm', Elements.scene.skyboxReflectionConvolutionBake.bind(panel));
panel.$.sceneSkyboxReflectionRemove.addEventListener('confirm', Elements.scene.skyboxReflectionConvolutionRemove.bind(panel));
},
async update() {
const panel = this;
if (!panel.dump || !panel.dump.isScene) {
if (!panel.isAnimationMode()) {
panel.toggleShowAddComponentBtn(true);
}
return;
}
// 场景模式要隐藏按钮
panel.toggleShowAddComponentBtn(false);
panel.$this.setAttribute('sub-type', 'scene');
panel.$.container.removeAttribute('droppable');
panel.$.sceneRelease.render(panel.dump.autoReleaseAssets);
// 由于场景属性对象不是继承于 Component 所以没有修饰器,displayName, help 数据在这里配置
panel.dump._globals.ambient.displayName = 'Ambient';
panel.dump._globals.ambient.editor = { help: 'i18n:cc.Ambient' };
panel.dump._globals.ambient.help = panel.getHelpUrl(panel.dump._globals.ambient.editor);
panel.$.sceneAmbient.render(panel.dump._globals.ambient);
panel.dump._globals.fog.displayName = 'Fog';
panel.dump._globals.fog.editor = { help: 'i18n:cc.Fog' };
panel.dump._globals.fog.help = panel.getHelpUrl(panel.dump._globals.fog.editor);
panel.$.sceneFog.render(panel.dump._globals.fog);
panel.dump._globals.shadows.displayName = 'Shadows';
panel.dump._globals.shadows.editor = { help: 'i18n:cc.Shadow' };
panel.dump._globals.shadows.help = panel.getHelpUrl(panel.dump._globals.shadows.editor);
panel.$.sceneShadows.render(panel.dump._globals.shadows);
// skyBox 逻辑 start
let $sceneSkyboxContainer = panel.$.sceneSkyboxBefore;
const oldSkyboxProps = Object.keys(panel.$skyboxProps);
const newSkyboxProps = [];
// these properties have custom editing interface
const customProperties = ['envmap', 'useHDR', '_envmapHDR', '_envmapLDR'];
const afterPositionProperties = ['reflectionMap', 'diffuseMap'];
for (const key in panel.dump._globals.skybox.value) {
const dump = panel.dump._globals.skybox.value[key];
if (customProperties.includes(key)) {
if (key === 'useHDR') {
panel.$.sceneSkyboxUseHDR.value = dump.value ? 'HDR' : 'LDR';
panel.$.sceneSkyboxUseHDR.dump = dump;
} else if (key === '_envmapHDR') {
panel.$.sceneSkyboxEnvmapHDR.render(dump);
} else if (key === '_envmapLDR') {
panel.$.sceneSkyboxEnvmapLDR.render(dump);
}
continue;
}
if (!dump.visible) {
continue;
}
const id = `${dump.type || dump.name}:${dump.path}`;
let $prop = panel.$skyboxProps[id];
newSkyboxProps.push(id);
if (afterPositionProperties.includes(key)) {
$sceneSkyboxContainer = panel.$.sceneSkyboxAfter;
} else {
$sceneSkyboxContainer = panel.$.sceneSkyboxBefore;
}
if (!$prop) {
$prop = document.createElement('ui-prop');
$prop.setAttribute('type', 'dump');
panel.$skyboxProps[id] = $prop;
$sceneSkyboxContainer.appendChild($prop);
} else if (!$prop.isConnected || !$prop.parentElement) {
$sceneSkyboxContainer.appendChild($prop);
}
$prop.render(dump);
}
for (const id of oldSkyboxProps) {
if (!newSkyboxProps.includes(id)) {
const $prop = panel.$skyboxProps[id];
if ($prop && $prop.parentElement) {
$prop.parentElement.removeChild($prop);
}
}
}
Elements.scene.skyboxReflectionConvolution.call(panel);
// skyBox 逻辑 end
panel.dump._globals.octree.displayName = 'Octree Scene Culling';
panel.dump._globals.octree.editor = { help: 'i18n:cc.OctreeCulling' };
panel.dump._globals.octree.help = panel.getHelpUrl(panel.dump._globals.octree.editor);
panel.$.sceneOctree.render(panel.dump._globals.octree);
panel.dump._globals.skin.displayName = 'Skin';
panel.dump._globals.skin.editor = { help: 'i18n:cc.Skin' };
panel.dump._globals.skin.help = panel.getHelpUrl(panel.dump._globals.skin.editor);
panel.$.sceneSkin.render(panel.dump._globals.skin);
panel.dump._globals.postSettings.displayName = 'PostSettings';
panel.$.scenePostSettings.render(panel.dump._globals.postSettings);
const $skyProps = panel.$.sceneSkybox.querySelectorAll('ui-prop[type="dump"]');
$skyProps.forEach(($prop) => {
if ($prop.dump.name === 'envLightingType') {
if (!$prop.regenerate) {
$prop.regenerate = Elements.scene.regenerate.bind(panel);
$prop.addEventListener('change-dump', $prop.regenerate);
}
}
});
},
async regenerate() {
const panel = this;
const dump = panel.dump._globals.skybox.value;
if (!dump.envmap.value) {
return;
}
const envMapUuid = dump.envmap.value.uuid;
if (!envMapUuid) {
return;
}
const envLightingType = dump.envLightingType.value;
// DIFFUSEMAP_WITH_REFLECTION 的枚举值为 2
if (envLightingType === 2) {
await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
name: 'inspector',
method: 'generateDiffuseMap',
args: [envMapUuid],
});
} else {
await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
name: 'inspector',
method: 'generateVector',
args: [envMapUuid],
});
}
},
async setEnvMapAndConvolutionMap(uuid) {
await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
name: 'inspector',
method: 'setSkyboxEnvMap',
args: [uuid],
});
if (uuid) {
await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
name: 'inspector',
method: 'setReflectionConvolutionMap',
args: [uuid],
});
}
},
async skyboxReflectionConvolution() {
const panel = this;
panel.$.sceneSkyboxReflection.style.display = 'inline-block';
panel.$.sceneSkyboxReflectionLoading.style.display = 'none';
const reflectionMap = panel.dump._globals.skybox.value['reflectionMap'];
if (reflectionMap.value && reflectionMap.value.uuid) {
panel.$.sceneSkyboxReflectionBake.style.display = 'none';
panel.$.sceneSkyboxReflectionRemove.style.display = 'inline-flex';
} else {
panel.$.sceneSkyboxReflectionBake.style.display = 'inline-flex';
panel.$.sceneSkyboxReflectionRemove.style.display = 'none';
// if envmap value unexist, the column of bake button hidden;
const envMapData = panel.dump._globals.skybox.value['envmap'];
if (!envMapData.value || !envMapData.value.uuid) {
panel.$.sceneSkyboxReflection.style.display = 'none';
}
}
},
async skyboxReflectionConvolutionBake() {
const panel = this;
const envMapData = panel.dump._globals.skybox.value['envmap'];
if (!envMapData.value || !envMapData.value.uuid) {
return;
}
panel.$.sceneSkyboxReflectionLoading.style.display = 'inline-flex';
panel.$.sceneSkyboxReflectionBake.style.display = 'none';
await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
name: 'inspector',
method: 'bakeReflectionConvolution',
args: [envMapData.value.uuid],
});
},
async skyboxReflectionConvolutionRemove() {
const panel = this;
const reflectionMap = panel.dump._globals.skybox.value['reflectionMap'];
if (reflectionMap.value && reflectionMap.value.uuid) {
const $skyProps = panel.$.sceneSkybox.querySelectorAll('ui-prop[type="dump"]');
for (const $skyProp of $skyProps) {
if ($skyProp.dump.name === 'reflectionMap') {
const textCubeAssetUuid = $skyProp.dump.value.uuid;
if (textCubeAssetUuid) {
// remove asset
try {
const imageAssetUuid = textCubeAssetUuid.split('@')[0];
const imageAssetUrl = await Editor.Message.request('asset-db', 'query-url', imageAssetUuid);
if (imageAssetUrl) {
await Editor.Message.request('asset-db', 'delete-asset', imageAssetUrl);
}
} catch (error) {
console.error(error);
}
$skyProp.dump.value.uuid = '';
$skyProp.dispatch('change-dump');
$skyProp.dispatch('confirm-dump'); // for scene snapshot
}
break;
}
}
}
},
skyboxUseHDRChange(event) {
const panel = this;
const $radioGraph = event.currentTarget;
const useHDR = $radioGraph.value === 'HDR';
$radioGraph.dump.value = useHDR;
$radioGraph.dispatch('change-dump');
$radioGraph.dispatch('confirm-dump'); // for scene snapshot
const $prop = useHDR ? panel.$.sceneSkyboxEnvmapHDR : panel.$.sceneSkyboxEnvmapLDR;
const uuid = $prop.dump.value.uuid;
Elements.scene.setEnvMapAndConvolutionMap.call(panel, uuid);
panel.$.scenePostSettings.style.display = useHDR ? 'block' : 'none';
},
skyboxEnvmapChange(useHDR, event) {
const panel = this;
if (panel.dump._globals.skybox.value['useHDR'].value !== useHDR) {
// 未选中项的变动,不需要后续执行
return;
}
const $prop = event.currentTarget;
const uuid = $prop.dump.value.uuid;
Elements.scene.setEnvMapAndConvolutionMap.call(panel, uuid);
},
},
node: {
ready() {
const panel = this;
panel.$skyboxProps = {};
panel.setHelpUrl(panel.$.nodeLink, { help: 'i18n:cc.Node' });
panel.$.nodeMenu.addEventListener('click', (event) => {
event.stopPropagation();
exports.methods.nodeContextMenu(panel.uuidList, panel.dumps);
});
panel.$.nodeLink.addEventListener('click', (event) => {
event.stopPropagation();
});
panel.i18nChangeBind = Elements.node.i18nChange.bind(panel);
Editor.Message.addBroadcastListener('i18n:change', panel.i18nChangeBind);
// 针对layer节点属性的右键菜单
panel.$.nodeLayer && panel.$.nodeLayer.addEventListener('contextmenu', (event) => {
event.stopPropagation();
event.preventDefault();
if (!panel.dump || !panel.dump.layer) { return; }
const layer = panel.dump.layer;
const store = Elements.node.getAndParseClipboard();
const pasteEnable = Elements.node.validatePasteEnable(layer, store);
Editor.Menu.popup({
menu: [
{
label: Editor.I18n.t('ENGINE.menu.copy_property_path'),
async click() {
if (layer.path) { clipboard.writeText(layer.path); }
},
},
{ type: 'separator' },
{
label: Editor.I18n.t('ENGINE.menu.copy_property_value'),
click() {
const { type = '', value, enumList } = layer;
const storeData = {
type,
value,
enumList,
};
clipboard.writeText(JSON.stringify(storeData));
},
},
{
label: Editor.I18n.t('ENGINE.menu.paste_property_value'),
enabled: pasteEnable,
click() {
const select = panel.$.nodeLayerSelect.querySelector('ui-select');
if (select) {
select.value = store.value;
layer.value = store.value;
if (layer.values) {
layer.values.forEach((val, index) => dump.values[index] = store.value);
}
select.dispatch('change');
select.dispatch('confirm');
}
},
},
],
});
});
},
async update() {
const panel = this;
panel.componentCacheExpand = {};
if (!panel.dump || panel.dump.isScene) {
return;
}
panel.$this.setAttribute('sub-type', 'node');
panel.$.container.setAttribute('droppable', panel.dropConfig && Object.keys(panel.dropConfig).join());
panel.$.nodePosition.render(panel.dump.position);
panel.$.nodeRotation.render(panel.dump.rotation);
panel.$.nodeScale.render(panel.dump.scale);
panel.$.nodeMobility.render(panel.dump.mobility);
setLabel(panel.dump.layer, panel.$.nodeLayer);
// 查找需要渲染的 component 列表
const componentList = [];
for (let i = 0; i < panel.dump.__comps__.length; i++) {
const comp = panel.dump.__comps__[i];
if (
panel.dumps.every((dump) => {
return dump.__comps__[i] && dump.__comps__[i].type === comp.type;
})
) {
componentList.push(comp);
}
}
const sectionBody = panel.$.sectionBody;
const isNotEmpty = componentList.length && sectionBody.__sections__ && sectionBody.__sections__.length;
const isSameLength = isNotEmpty && sectionBody.__sections__.length === componentList.length;
const isAllSameType =
isSameLength &&
componentList.every((comp, i) => {
return (
comp.type === sectionBody.__sections__[i].__type__ &&
comp.mountedRoot === sectionBody.__sections__[i].dump?.mountedRoot
);
});
// 如果元素长度、类型一致,则直接更新现有的界面
if (isAllSameType) {
for (let index = 0; index < sectionBody.__sections__.length; index++) {
const $section = sectionBody.__sections__[index];
const dump = componentList[index];
$section.dump = dump;
// 处理 ui-checkbox 涉及多选的情况
const $active = $section.querySelector('ui-checkbox');
$active.dump = dump.value.enabled;
$active.value = dump.value.enabled.value;
if ($active.dump.values && $active.dump.values.some((ds) => ds !== $active.dump.value)) {
$active.invalid = true;
} else {
$active.invalid = false;
}
const $link = $section.querySelector('ui-link');
panel.setHelpUrl($link, dump.editor);
await Promise.all($section.__panels__.map(($panel) => {
return $panel.update(dump);
}));
}
} else {
// 如果元素不一致,说明切换了选中元素,那么需要更新整个界面
sectionBody.innerText = '';
sectionBody.__sections__ = [];
componentList.forEach(async (component, i) => {
const additional = JSON.stringify([{
type: component.type,
value: component.value.uuid.value,
}]);
const $section = document.createElement('ui-section');
$section.setAttribute('expand', '');
$section.setAttribute('class', 'component config');
let cacheExpandKey = `node-component:${component.type}`;
if (panel.componentCacheExpand[cacheExpandKey]) {
// when exist duplicated component, use uuid as key;
cacheExpandKey = `node-component:${component.value.uuid.value}`;
}
panel.componentCacheExpand[cacheExpandKey] = true;
$section.setAttribute('cache-expand', `${cacheExpandKey}`);
$section.innerHTML = `
`;
$section.dump = component;
$section.__panels__ = [];
$section.__type__ = component.type;
const $active = $section.querySelector('ui-checkbox');
$active.value = component.value.enabled.value;
$active.dump = component.value.enabled;
$active.addEventListener('change', (event) => {
event.stopPropagation();
const value = !!$active.value;
const dump = $active.dump;
dump.value = value;
if ('values' in dump) {
dump.values.forEach((val, index) => {
dump.values[index] = value;
});
}
$active.dispatch('change-dump');
});
$active.addEventListener('confirm', (event) => {
event.stopPropagation();
$active.dispatch('confirm-dump');
});
const $link = $section.querySelector('.link');
$link.addEventListener('click', (event) => {
event.stopPropagation();
});
panel.setHelpUrl($link, component.editor);
const $menu = $section.querySelector('.menu');
$menu.addEventListener('click', (event) => {
event.stopPropagation();
exports.methods.componentContextMenu(panel.uuidList, $section.dump, componentList.length, i, panel.dumps);
});
sectionBody.__sections__[i] = $section;
sectionBody.appendChild($section);
// 排序
const renderListHeader = panel.renderMap.header[$section.__type__] ?? [];
let renderListSection = panel.renderMap.section[$section.__type__] ?? [];
const renderListFooter = panel.renderMap.footer[$section.__type__] ?? [];
// 如果都没有渲染模板,使用默认 cc.Class 模板
if (!renderListSection.length) {
// 判断继承
if (Array.isArray(component.extends)) {
const parentClass = component.extends[0];
renderListSection = panel.renderMap.section[parentClass];
}
if (!renderListSection) {
renderListSection = panel.renderMap.section['cc.Class'];
}
}
let renderList = [...renderListHeader, ...renderListSection, ...renderListFooter];
renderList.forEach((file) => {
const $panel = document.createElement('ui-panel');
$panel.injectionStyle(injectionStyle);
$panel.setAttribute('src', file);
$panel.shadowRoot.addEventListener('change-dump', (event) => {
exports.listeners['change-dump'].call(panel, event);
});
$panel.shadowRoot.addEventListener('confirm-dump', (event) => {
exports.listeners['confirm-dump'].call(panel, event);
});
$panel.shadowRoot.addEventListener('reset-dump', (event) => {
exports.listeners['reset-dump'].call(panel, event);
});
$panel.shadowRoot.addEventListener('create-dump', (event) => {
exports.listeners['create-dump'].call(panel, event);
});
$panel.shadowRoot.addEventListener('preview-dump', (event) => {
exports.listeners['preview-dump'].call(panel, event);
});
$section.appendChild($panel);
$section.__panels__.push($panel);
$panel.dump = component;
$panel.messageProtocol = messageProtocol;
$panel.update(component);
});
// 组件丢失的提示
if (component.type === "cc.MissingScript") {
const $missTip = document.createElement('div');
$missTip.style.cssText = "border: 1px solid var(--color-normal-border); padding: 15px; border-radius: 4px;margin-top: 15px;";
const assetData = await Editor.Message.request('asset-db', 'query-asset-data', component.value.__scriptAsset.value.uuid);
$missTip.innerHTML = `${assetData ? assetData.url : ''} ${Editor.I18n.t('ENGINE.components.missScriptTip')}`;
$section.appendChild($missTip);
}
});
}
// 自定义 node 数据
if (panel.renderMap.section && panel.renderMap.section['cc.Node']) {
const array = (panel.$.nodeSection.__node_panels__ = panel.$.nodeSection.__node_panels__ || []);
panel.renderMap.section['cc.Node'].forEach((file, index) => {
if (!array[index]) {
array[index] = document.createElement('ui-panel');
array[index].injectionStyle(injectionStyle);
panel.$.nodeSection.appendChild(array[index]);
}
array[index].setAttribute('src', file);
array[index].dump = panel.dump;
array[index].messageProtocol = messageProtocol;
array[index].update(panel.dump);
});
for (let i = panel.renderMap.section['cc.Node'].length; i < array.length; i++) {
array[i].remove();
}
array.length = panel.renderMap.section['cc.Node'].length;
} else if (panel.$.nodeSection.__node_panels__) {
panel.$.nodeSection.__node_panels__.forEach((dom) => {
dom.remove();
});
delete panel.$.nodeSection.__node_panels__;
}
},
close() {
const panel = this;
Editor.Message.removeBroadcastListener('i18n:change', panel.i18nChangeBind);
},
i18nChange() {
const panel = this;
const $links = panel.$.container.querySelectorAll('ui-link');
$links.forEach($link => panel.setHelpUrl($link));
},
getAndParseClipboard() {
const store = clipboard.readText();
if (!store) { return; }
try {
return JSON.parse(store);
} catch (err) {
return;
}
},
validatePasteEnable(dump, store) {
if (!store) { return false; }
const { type, value, enumList = [], bitmaskList = [] } = store;
if (typeof type === 'undefined' || typeof value === 'undefined') { return false; }
if (type !== dump.type || Boolean(dump.isArray) !== Array.isArray(value) || dump.readonly) { return false; }
switch (type) {
case 'BitMask': {
return bitmaskList.length === dump.bitmaskList?.length && bitmaskList.every((item, index) => {
return item.name === dump.bitmaskList?.[index].name && item.value === dump.bitmaskList?.[index].value;
});
}
case 'Enum': {
return enumList.length === dump.enumList?.length && enumList.every((item, index) => {
return item.name === dump.enumList?.[index].name && item.value === dump.enumList?.[index].value;
}) && enumList.some(item => item.value === value);
}
default: return true;
}
},
},
missingComponent: {
ready() {
const panel = this;
const sectionMissing = panel.$.sectionMissing;
sectionMissing.addEventListener('click', (event) => {
if (event.target.tagName !== 'UI-ICON') {
return;
}
if (!Array.isArray(panel.dump.removedComponents)) {
return;
}
const i = event.target.getAttribute('index');
const type = event.target.getAttribute('value');
const info = panel.dump.removedComponents[i];
if (!info) {
return;
}
const uuidList = panel.uuidList;
switch (type) {
case 'save-o': {
Editor.Message.request(messageProtocol.scene, 'apply-removed-component', uuidList[0], info.fileID);
break;
}
case 'reset': {
Editor.Message.request(messageProtocol.scene, 'revert-removed-component', uuidList[0], info.fileID);
break;
}
}
});
},
update() {
const panel = this;
if (!panel.dump || panel.dump.isScene) {
return;
}
const uuidList = panel.uuidList;
const sectionMissing = panel.$.sectionMissing;
sectionMissing.__sections__ = sectionMissing.__sections__ || [];
if (!panel.dump.removedComponents || uuidList.length !== 1) {
panel.dump.removedComponents = [];
}
for (let i = 0; i < panel.dump.removedComponents.length; i++) {
let $section = sectionMissing.__sections__[i];
if (!$section) {
$section = document.createElement('section');
sectionMissing.__sections__[i] = $section;
sectionMissing.appendChild($section);
}
$section.innerHTML = `
${panel.dump.removedComponents[i].name} [removed]
`;
}
while (sectionMissing.__sections__.length > panel.dump.removedComponents.length) {
const $section = sectionMissing.__sections__.pop();
$section.parentElement.removeChild($section);
}
},
},
layer: {
ready() {
const panel = this;
panel.$.nodeLayerButton.addEventListener('change', (event) => {
event.stopPropagation();
Editor.Message.send('project', 'open-settings', 'project', 'layer');
});
},
update() {
const panel = this;
if (!panel.dump || panel.dump.isScene) {
return;
}
panel.$.nodeLayerSelect.render(panel.dump.layer);
let prevValues = [panel.dump.layer.value];
if (panel.dump.layer.values) {
prevValues = panel.dump.layer.values.slice();
}
panel.$.nodeLayerSelect.prevValues = prevValues;
},
},
footer: {
ready() {
const panel = this;
panel.$.componentAdd.addEventListener('click', () => {
Editor.Panel.__protected__.openKit('ui-kit.searcher', {
elem: panel.$.componentAdd,
params: [
{
type: 'add-component',
},
],
listeners: {
async confirm(detail/* info */) {
if (!detail) { return; }
// 批量调用request意味着编辑操作在很多帧后才会完成,所以不能自动记录undo
const undoID = await beginRecording(panel.uuidList);
for (const uuid of panel.uuidList) {
await Editor.Message.request(messageProtocol.scene, 'create-component', {
uuid,
component: detail.info.cid,
});
}
if (detail.info.name) {
trackEventWithTimer('laber', `A100000_${detail.info.name}`);
}
await endRecording(undoID);
},
},
});
});
},
update() {},
},
materials: {
async update() {
const panel = this;
const materialPanels = [];
const materialPanelType = 'asset';
const oldChildren = Array.from(panel.$.sectionAsset.children);
const materialUuids = panel.assets['cc.Material'];
let materialPrevPanel = null;
for (const materialUuid in materialUuids) {
let materialPanel = oldChildren.find((child) => child.getAttribute('uuid') === materialUuid);
if (!materialPanel) {
// 添加新的
materialPanel = document.createElement('ui-panel');
materialPanel.injectionStyle(injectionStyle);
materialPanel.setAttribute('src', panel.typeManager[materialPanelType]);
materialPanel.setAttribute('type', materialPanelType);
materialPanel.setAttribute('sub-type', 'unknown');
materialPanel.setAttribute('uuid', materialUuid);
materialPanel.panelObject.replaceContainerWithUISection({
type: materialPanelType,
uuid: materialUuid,
});
const { section = {} } = panel.renderManager[materialPanelType];
// 按数组顺序放置
if (materialPrevPanel) {
materialPrevPanel.after(materialPanel);
} else {
panel.$.sectionAsset.prepend(materialPanel);
}
// call update after panel is connected(ensure lifecycle hook `ready` has been called)
materialPanel.update([materialUuid], { section });
materialPanel.focusEventInNode = () => {
const children = Array.from(materialPanel.parentElement.children);
children.forEach((child) => {
if (child === materialPanel) {
child.setAttribute('focused', '');
} else {
child.removeAttribute('focused');
}
});
};
materialPanel.blurEventInNode = () => {
if (panel.blurSleep) {
return;
}
materialPanel.removeAttribute('focused');
};
materialPanel.addEventListener('focus', materialPanel.focusEventInNode);
materialPanel.addEventListener('blur', materialPanel.blurEventInNode);
}
materialPanels.push(materialPanel);
materialPrevPanel = materialPanel;
}
// 删除多余的
for (const oldChild of oldChildren) {
if (oldChild && materialPanels.indexOf(oldChild) === -1) {
await oldChild.panel.beforeClose.call(oldChild.panelObject);
oldChild.removeEventListener('focus', oldChild.focusEventInNode);
oldChild.removeEventListener('blur', oldChild.blurEventInNode);
oldChild.focusEventInNode = undefined;
oldChild.blurEventInNode = undefined;
oldChild.remove();
}
}
},
async beforeClose() {
const panel = this;
const children = Array.from(panel.$.sectionAsset.children);
for (const materialPanel of children) {
const next = await materialPanel.panel.beforeClose.call(materialPanel.panelObject);
if (next === false) {
return false;
} else {
materialPanel.removeEventListener('focus', materialPanel.focusEventInNode);
materialPanel.removeEventListener('blur', materialPanel.blurEventInNode);
materialPanel.focusEventInNode = undefined;
materialPanel.blurEventInNode = undefined;
materialPanel.remove();
}
}
return true;
},
},
};
exports.methods = {
undo() {
this.restore('undo');
},
redo() {
this.restore('redo');
},
restore(cmd) {
if (!cmd) {
return;
}
const panel = this;
panel.blurSleep = true;
clearTimeout(panel.blurSleepTimeId);
panel.blurSleepTimeId = setTimeout(() => {
panel.blurSleep = false;
}, 1000);
const children = Array.from(panel.$.sectionAsset.children);
for (const materialPanel of children) {
if (materialPanel.hasAttribute('focused')) {
materialPanel.panelObject[cmd]();
return;
}
}
Editor.Message.send(messageProtocol.scene, cmd);
},
setHelpUrl($link, data) {
if (data) {
$link.helpData = data;
} else {
if (!$link.helpData) {
return;
}
data = $link.helpData;
}
const url = this.getHelpUrl(data);
if (url) {
$link.setAttribute('value', url);
} else {
$link.removeAttribute('value');
}
},
/**
* 获取组件帮助菜单的 url
* @param editor
*/
getHelpUrl(data) {
if (!data || !data.help) {
return '';
}
const help = data.help;
/**
* 约定的规则
* 翻译的都需要 i18n: 开头
* 没有的话属于直接是配置值的方式,配什么返回什么
*/
if (!help.startsWith('i18n:')) {
return help;
}
const i18nKey = help.substr(5);
const url = Editor.I18n.t('ENGINE.help.' + i18nKey);
if (url) {
return url;
}
/**
* 再在编辑器内部查找翻译一次
* 结果可能为空,也是一种需求,即组件配置了但没有合适的文档配置
*/
return Editor.I18n.t(i18nKey);
},
/**
* 组件上的右键菜单
*/
componentContextMenu(uuidList, dump, total, index, nodeDumps) {
// 是否多选节点
const isMultiple = uuidList.length > 1 ? true : false;
const uuid = uuidList[0];
const clipboardComponentInfo = Editor.Clipboard.read('_dump_component_');
Editor.Menu.popup({
menu: [
{
label: Editor.I18n.t('ENGINE.menu.reset_component'),
async click() {
const values = dump.value.uuid.values || [dump.value.uuid.value];
const undoID = await beginRecording(values);
for (const compUuid of values) {
await Editor.Message.request(messageProtocol.scene, 'reset-component', {
uuid: compUuid,
});
}
await endRecording(undoID);
},
},
{ type: 'separator' },
{
label: Editor.I18n.t('ENGINE.menu.remove_component'),
async click() {
const values = dump.value.uuid.values || [dump.value.uuid.value];
// 收集待修改的uuids
const uuids = [];
const indexes = [];
for (const value of values) {
for (const nodeDump of nodeDumps) {
const uuid = nodeDump.uuid.value;
const index = nodeDump.__comps__.findIndex((dumpData) => dumpData.value.uuid.value === value);
if (index !== -1) {
uuids.push(uuid);
indexes.push(index);
if (nodeDump.__comps__[index].type) {
trackEventWithTimer('laber', `A100001_${nodeDump.__comps__[index].type}`);
}
}
}
}
if (!uuids.length > 0) { return; }
const undoID = await beginRecording(uuids);
for (let i = 0; i < uuids.length; i++) {
await Editor.Message.request(messageProtocol.scene, 'remove-array-element', {
uuid: uuids[i],
path: '__comps__',
index: indexes[i],
});
}
await endRecording(undoID);
},
},
{
label: Editor.I18n.t('ENGINE.menu.move_up_component'),
enabled: !isMultiple && index !== 0,
async click() {
const undoID = await beginRecording(uuid);
await Editor.Message.request(messageProtocol.scene, 'move-array-element', {
uuid,
path: '__comps__',
target: index,
offset: -1,
});
await endRecording(undoID);
},
},
{
label: Editor.I18n.t('ENGINE.menu.move_down_component'),
enabled: !isMultiple && index !== total - 1,
async click() {
const undoID = await beginRecording(uuid);
await Editor.Message.request(messageProtocol.scene, 'move-array-element', {
uuid,
path: '__comps__',
target: index,
offset: 1,
});
await endRecording(undoID);
},
},
{ type: 'separator' },
{
label: Editor.I18n.t('ENGINE.menu.copy_component'),
enabled: !isMultiple,
click() {
const info = JSON.parse(JSON.stringify(dump));
delete info.value.__prefab;
Editor.Clipboard.write('_dump_component_', {
cid: dump.cid,
dump: info,
});
},
},
{
label: Editor.I18n.t('ENGINE.menu.paste_component_values'),
enabled: !!(clipboardComponentInfo && clipboardComponentInfo.cid === dump.cid),
async click() {
const values = dump.value.uuid.values || [dump.value.uuid.value];
const uuids = [];
const indexes = [];
for (const value of values) {
for (const nodeDump of nodeDumps) {
const uuid = nodeDump.uuid.value;
const index = nodeDump.__comps__.findIndex((dumpData) => dumpData.value.uuid.value === value);
if (index !== -1) {
uuids.push(uuid);
indexes.push(index);
}
}
}
const undoID = await beginRecording(uuids);
// 遍历uuids
for (let i = 0; i < uuids.length; i++) {
const uuid = uuids[i];
const index = indexes[i];
const nodeDump = nodeDumps.find(nodeDump => uuid === nodeDump.uuid.value);
await Editor.Message.request(messageProtocol.scene, 'set-property', {
uuid,
path: nodeDump.__comps__[index].path,
dump: clipboardComponentInfo.dump,
});
}
await endRecording(undoID);
},
},
{ type: 'separator' },
{
// 这个按钮不该出现在 component 上,应该在节点上
label: Editor.I18n.t('ENGINE.menu.paste_component'),
enabled: !!clipboardComponentInfo,
async click() {
const undoID = await beginRecording(uuidList);
const values = dump.value.uuid.values || [dump.value.uuid.value];
let index = 0;
for (const dump of values) {
const uuid = uuidList[index];
await Editor.Message.request(messageProtocol.scene, 'create-component', {
uuid,
component: clipboardComponentInfo.cid,
});
// 检查是否创建成功,是的话,给赋值
const nodeDump = await Editor.Message.request(messageProtocol.scene, 'query-node', uuid);
const length = nodeDump.__comps__ && nodeDump.__comps__.length;
if (length) {
const lastIndex = length - 1;
const lastComp = nodeDump.__comps__[lastIndex];
if (lastComp?.cid === clipboardComponentInfo.cid) {
await Editor.Message.request(messageProtocol.scene, 'set-property', {
uuid,
path: `__comps__.${lastIndex}`,
dump: clipboardComponentInfo.dump,
});
}
}
index++;
}
await endRecording(undoID);
},
},
],
});
},
nodeContextMenu(uuidList, dumps) {
const dump = dumps[0];
// 是否多选节点
const isMultiple = dump.length > 1 ? true : false;
const clipboardNodeInfo = Editor.Clipboard.read('_dump_node_');
const clipboardNodeWorldTransform = Editor.Clipboard.read('_dump_node_world_transform_');
const clipboardComponentInfo = Editor.Clipboard.read('_dump_component_');
function notEqualDefaultValueVec3(propName) {
const keys = ['x', 'y', 'z'];
return keys.some(key => {
return dump[propName].value[key] !== dump[propName].default.value[key].value;
});
}
Editor.Menu.popup({
menu: [
{
label: Editor.I18n.t('ENGINE.menu.reset_node'),
enabled: !dump.position.readonly && !dump.rotation.readonly && !dump.scale.readonly,
async click() {
const undoID = await beginRecording(uuidList);
for (const uuid of uuidList) {
await Editor.Message.request(messageProtocol.scene, 'reset-node', {
uuid,
});
}
await endRecording(undoID);
},
},
{ type: 'separator' },
{
label: Editor.I18n.t('ENGINE.menu.copy_node_value'),
enabled: !isMultiple,
async click() {
Editor.Clipboard.write('_dump_node_', {
type: dump.type,
attrs: ['position', 'rotation', 'scale', 'mobility', 'layer'],
dump: JSON.parse(JSON.stringify(dump)),
});
},
},
{
label: Editor.I18n.t('ENGINE.menu.paste_node_value'),
enabled: !!clipboardNodeInfo,
async click() {
const undoID = await beginRecording(uuidList);
for (const uuid of uuidList) {
for (const attr of clipboardNodeInfo.attrs) {
await Editor.Message.request(messageProtocol.scene, 'set-property', {
uuid,
path: attr,
dump: clipboardNodeInfo.dump[attr],
});
}
}
await endRecording(undoID);
},
},
{ type: 'separator' },
{
label: Editor.I18n.t('ENGINE.menu.copy_node_world_transform'),
enabled: !isMultiple,
async click() {
const data = await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
name: 'inspector',
method: 'queryNodeWorldTransform',
args: [dump.uuid.value],
});
if (data) {
Editor.Clipboard.write('_dump_node_world_transform_', {
data,
});
}
},
},
{
label: Editor.I18n.t('ENGINE.menu.paste_node_world_transform'),
enabled: !!clipboardNodeWorldTransform,
async click() {
if (clipboardNodeWorldTransform.data) {
const undoID = await beginRecording(uuidList);
for (const uuid of uuidList) {
await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
name: 'inspector',
method: 'setNodeWorldTransform',
args: [uuid, clipboardNodeWorldTransform.data],
});
}
await endRecording(undoID);
}
},
},
{ type: 'separator' },
{
label: Editor.I18n.t('ENGINE.menu.paste_component'),
enabled: !!clipboardComponentInfo,
async click() {
const undoID = await beginRecording(uuidList);
for (const uuid of uuidList) {
await Editor.Message.request(messageProtocol.scene, 'create-component', {
uuid,
component: clipboardComponentInfo.cid,
});
// 检查是否创建成功,是的话,给赋值
const nodeDump = await Editor.Message.request(messageProtocol.scene, 'query-node', uuid);
const length = nodeDump.__comps__ && nodeDump.__comps__.length;
if (length) {
const lastIndex = length - 1;
const lastComp = nodeDump.__comps__[lastIndex];
if (lastComp?.cid === clipboardComponentInfo.cid) {
await Editor.Message.request(messageProtocol.scene, 'set-property', {
uuid,
path: `__comps__.${lastIndex}`,
dump: clipboardComponentInfo.dump,
});
}
}
}
await endRecording(undoID);
},
},
{ type: 'separator' },
{
label: Editor.I18n.t('ENGINE.menu.reset_node_position'),
enabled: !dump.position.readonly && notEqualDefaultValueVec3('position'),
async click() {
const undoID = await beginRecording(uuidList);
for (const uuid of uuidList) {
await Editor.Message.request(messageProtocol.scene, 'reset-property', {
uuid,
path: 'position',
});
}
await endRecording(undoID);
},
},
{
label: Editor.I18n.t('ENGINE.menu.reset_node_rotation'),
enabled: !dump.rotation.readonly && notEqualDefaultValueVec3('rotation'),
async click() {
const undoID = await beginRecording(uuidList);
for (const uuid of uuidList) {
await Editor.Message.request(messageProtocol.scene, 'reset-property', {
uuid,
path: 'rotation',
});
}
await endRecording(undoID);
},
},
{
label: Editor.I18n.t('ENGINE.menu.reset_node_scale'),
enabled: !dump.scale.readonly && notEqualDefaultValueVec3('scale'),
async click() {
const undoID = await beginRecording(uuidList);
for (const uuid of uuidList) {
await Editor.Message.request(messageProtocol.scene, 'reset-property', {
uuid,
path: 'scale',
});
}
await endRecording(undoID);
},
},
{
label: Editor.I18n.t('ENGINE.menu.reset_node_mobility'),
enabled: !dump.mobility.readonly && dump.mobility.value !== dump.mobility.default,
async click() {
const undoID = await beginRecording(uuidList);
for (const uuid of uuidList) {
await Editor.Message.request(messageProtocol.scene, 'reset-property', {
uuid,
path: 'mobility',
});
}
await endRecording(undoID);
},
},
],
});
},
async replaceAssetUuidInNodes(assetUuid, newAssetUuid) {
const panel = this;
const materialUuids = panel.assets['cc.Material'];
if (!materialUuids) {
return;
}
try {
const undoID = await beginRecording(panel.uuidList);
for (const dumpPath in materialUuids[assetUuid]) {
const dumpData = materialUuids[assetUuid][dumpPath];
for (let i = 0; i < panel.uuidList.length; i++) {
const nodeUuid = panel.uuidList[i];
await Editor.Message.request(messageProtocol.scene, 'set-property', {
uuid: nodeUuid,
path: dumpPath,
dump: {
type: dumpData.type,
value: { uuid: newAssetUuid },
},
});
}
}
await endRecording(undoID);
} catch (error) {
console.error(error);
}
},
toggleShowAddComponentBtn(show) {
this.$.componentAdd.style.display = show ? 'inline-flex' : 'none';
},
isAnimationMode() {
return Editor.EditMode.getMode() === 'animation';
},
handlerSceneChangeMode() {
this.toggleShowAddComponentBtn(!this.isAnimationMode()); // 动画编辑模式下,要隐藏按钮
},
};
exports.update = async function update(uuidList, renderMap, dropConfig, typeManager, renderManager) {
const panel = this;
const enginePath = path.join('editor', 'inspector', 'components');
Object.values(renderMap).forEach((config) => {
Object.values(config).forEach((renders) => {
renders.sort((a, b) => {
return b.indexOf(enginePath) - a.indexOf(enginePath);
});
});
});
panel.uuidList = uuidList || [];
panel.renderMap = renderMap;
panel.dropConfig = dropConfig;
panel.typeManager = typeManager;
panel.renderManager = renderManager;
for (const prop in Elements) {
const element = Elements[prop];
if (element.update) {
await element.update.call(panel);
}
}
};
exports.ready = async function ready() {
const panel = this;
// 为了避免把 ui-num-input, ui-color 的连续 change 进行 snapshot
panel.snapshotLock = false;
// 节点的 ipc 协议,指向 scene 或 xr-scene 等进程
panel.__queryMessageProtocolScene__ = async function() {
try {
if (!panel.messageProtocol) {
panel.messageProtocol = messageProtocol;
}
const config = await await Editor.Profile.getConfig('inspector', 'message-protocol');
if (config) {
Object.assign(messageProtocol, config);
}
} catch (error) {
console.error(error);
messageProtocol.scene = 'scene';
}
};
await panel.__queryMessageProtocolScene__();
for (const prop in Elements) {
const element = Elements[prop];
if (element.ready) {
element.ready.call(panel);
}
}
this.replaceAssetUuidInNodesBind = this.replaceAssetUuidInNodes.bind(this);
this.handlerSceneChangeModeBind = this.handlerSceneChangeMode.bind(this);
Editor.Message.addBroadcastListener('inspector:replace-asset-uuid-in-nodes', this.replaceAssetUuidInNodesBind);
Editor.Message.addBroadcastListener('scene:change-mode', this.handlerSceneChangeModeBind);
};
exports.close = async function close() {
const panel = this;
for (const prop in Elements) {
const element = Elements[prop];
if (element.close) {
element.close.call(panel);
}
}
Editor.Message.removeBroadcastListener('inspector:replace-asset-uuid-in-nodes', this.replaceAssetUuidInNodesBind);
Editor.Message.removeBroadcastListener('scene:change-mode', this.handlerSceneChangeModeBind);
};
exports.beforeClose = async function beforeClose() {
const panel = this;
for (const prop in Elements) {
const element = Elements[prop];
if (element.beforeClose) {
const next = await element.beforeClose.call(panel);
if (next === false) {
return false;
}
}
}
return true;
};
exports.config = {
section: require('../components.js'),
footer: require('../components-footer.js'),
};