'use strict'; const fs = require('fs'); const path = require('path'); const { injectionStyle } = require('../utils/prop'); const History = require('./asset-history/index'); exports.listeners = {}; exports.style = fs.readFileSync(path.join(__dirname, './asset.css'), 'utf8'); exports.template = `
`; exports.$ = { container: '.container', header: '.header', content: '.content', location: '.location', copy: '.copy', assetThumbnail: '.asset-thumbnail', name: '.name', help: '.help', save: '.save', reset: '.reset', contentHeader: '.content-header', contentSection: '.content-section', contentFooter: '.content-footer', }; const Elements = { panel: { ready() { const panel = this; panel.__assetChangedHandle__ = undefined; panel.__assetChanged__ = (uuid) => { if (Array.isArray(panel.uuidList) && panel.uuidList.includes(uuid)) { window.cancelAnimationFrame(panel.__assetChangedHandle__); panel.__assetChangedHandle__ = window.requestAnimationFrame(async () => { await panel.reset(); }); } }; Editor.Message.addBroadcastListener('asset-db:asset-change', panel.__assetChanged__); panel.i18nChangeBind = Elements.panel.i18nChange.bind(panel); Editor.Message.addBroadcastListener('i18n:change', panel.i18nChangeBind); panel.history = new History(); }, async update() { const panel = this; let assetList = []; try { assetList = await Promise.all( panel.uuidList.map((uuid) => { return Editor.Message.request('asset-db', 'query-asset-info', uuid); }), ); } catch (err) { console.error(err); } assetList = assetList.filter(Boolean); panel.asset = assetList[0]; panel.assetList = []; panel.uuidList = []; panel.type = 'unknown'; if (panel.asset) { // 以第一个资源的类型,过滤多选的其他不同资源; 过滤只读资源的多选 const type = panel.asset.importer; assetList.forEach((asset) => { if (asset.importer === type) { if (panel.uuidList.length > 0 && asset.readonly) { return; } panel.uuidList.push(asset.uuid); panel.assetList.push(asset); } }); } // 判断数据合法性 if (!panel.asset) { panel.$.container.style.display = 'none'; } else { panel.$.container.style.display = 'flex'; panel.type = panel.asset.importer; if (panel.assetList.some((asset) => asset.importer !== panel.type)) { panel.type = 'unknown'; } } panel.$this.setAttribute('sub-type', panel.type); if (panel.type === 'unknown') { panel.metaList = []; panel.metaListOrigin = []; return; } try { panel.metaList = await Promise.all( panel.uuidList.map((uuid) => { return Editor.Message.request('asset-db', 'query-asset-meta', uuid); }), ); } catch (err) { console.error(err); panel.metaList = []; } panel.metaList = panel.metaList.filter(Boolean); panel.metaListOrigin = panel.metaList.map((meta) => { return JSON.stringify(meta); }); panel.setHelpUrl(panel.$.help, { help: panel.type }); }, close() { const panel = this; if (panel.__assetChangedHandle__) { window.cancelAnimationFrame(panel.__assetChangedHandle__); panel.__assetChangedHandle__ = undefined; } Editor.Message.removeBroadcastListener('i18n:change', panel.i18nChangeBind); Editor.Message.removeBroadcastListener('asset-db:asset-change', panel.__assetChanged__); delete panel.history; }, i18nChange() { const panel = this; const $links = panel.$.container.querySelectorAll('ui-link'); $links.forEach($link => panel.setHelpUrl($link)); }, }, header: { ready() { const panel = this; panel.$.save.addEventListener('click', (event) => { event.stopPropagation(); panel.save(); }); panel.$.reset.addEventListener('click', (event) => { event.stopPropagation(); panel.reset(); }); panel.$.copy.addEventListener('click', async (event) => { event.stopPropagation(); const assetsDir = path.join(Editor.Project.path, 'assets'); const result = await Editor.Dialog.select({ path: assetsDir, type: 'directory', }); let filePath = result.filePaths[0]; if (!filePath) { return; } filePath = path.join(filePath, panel.asset.name); // 必须保存在 /assets 文件夹下 if (!Editor.Utils.Path.contains(assetsDir, filePath)) { await Editor.Dialog.warn(Editor.I18n.t('ENGINE.dialog.warn'), { detail: Editor.I18n.t('ENGINE.inspector.cloneToDirectoryIllegal'), buttons: [Editor.I18n.t('ENGINE.dialog.confirm')], }); return; } const target = await Editor.Message.request('asset-db', 'query-url', filePath); if (target) { const asset = await Editor.Message.request('asset-db', 'copy-asset', panel.asset.url, target); if (asset) { const lastSelectType = Editor.Selection.getLastSelectedType(); if (lastSelectType === 'asset') { // 纯资源模式下 Editor.Selection.clear(lastSelectType); Editor.Selection.select(lastSelectType, asset.uuid); } else if (lastSelectType === 'node') { // 节点里使用资源的情况下,如材质 Editor.Message.broadcast('inspector:replace-asset-uuid-in-nodes', panel.asset.uuid, asset.uuid); } } } }); panel.$.assetThumbnail.addEventListener('click', (event) => { event.stopPropagation(); panel.uuidList.forEach((uuid) => { Editor.Message.request('assets', 'ui-kit:touch-asset', uuid); }); }); panel.$.location.addEventListener('click', (event) => { event.stopPropagation(); panel.uuidList.forEach((uuid) => { Editor.Message.request('assets', 'ui-kit:touch-asset', uuid); }); }); }, update() { const panel = this; if (!panel.asset) { return; } panel.$.name.value = panel.assetList.length === 1 ? panel.asset.name : `${panel.assetList.length} selections`; if (panel.asset.readonly) { panel.$.name.setAttribute('tooltip', 'i18n:inspector.asset.prohibitEditInternalAsset'); panel.$.name.setAttribute('readonly', ''); if (panel.asset.source && panel.asset.importer !== 'database') { panel.$.copy.style.display = 'inline-flex'; } else { panel.$.copy.style.display = 'none'; } } else { panel.$.name.removeAttribute('tooltip'); panel.$.name.removeAttribute('readonly'); panel.$.copy.style.display = 'none'; } panel.$.assetThumbnail.value = panel.asset.uuid; }, async isDirty() { const panel = this; const isDirty = await panel.isDirty(); if (isDirty) { panel.$.header.setAttribute('dirty', ''); } else { panel.$.header.removeAttribute('dirty'); } }, }, content: { ready() { const panel = this; panel.contentRenders = {}; }, async update() { const panel = this; // 重置渲染对象 panel.contentRenders = { header: { list: [], contentRender: panel.$.contentHeader, }, section: { list: panel.renderMap.section['unknown'], contentRender: panel.$.contentSection, }, footer: { list: [], contentRender: panel.$.contentFooter, }, }; for (const renderName in panel.renderMap) { if (panel.renderMap[renderName] && panel.renderMap[renderName][panel.type]) { panel.contentRenders[renderName].list = panel.renderMap[renderName][panel.type]; } } for (const renderName in panel.contentRenders) { const { list, contentRender } = panel.contentRenders[renderName]; contentRender.__panels__ = Array.from(contentRender.children).filter((el) => el.tagName === 'UI-PANEL'); let i = 0; for (i; i < list.length; i++) { const file = list[i]; if (!contentRender.__panels__[i]) { contentRender.__panels__[i] = document.createElement('ui-panel'); contentRender.__panels__[i].injectionStyle(injectionStyle); contentRender.__panels__[i].addEventListener('change', () => { Elements.header.isDirty.call(panel); }); contentRender.__panels__[i].addEventListener('snapshot', () => { panel.history && panel.history.snapshot(panel); }); contentRender.appendChild(contentRender.__panels__[i]); } contentRender.__panels__[i].setAttribute('src', file); } // 清除尾部多余的节点 for (i; i < contentRender.__panels__.length; i++) { contentRender.removeChild(contentRender.__panels__[i]); } try { await Promise.all( contentRender.__panels__.map(($panel) => { return $panel.update(panel.assetList, panel.metaList); }), ); } catch (err) { console.error(err); } } }, }, }; exports.methods = { undo() { const panel = this; panel.history && panel.history.undo(); }, redo() { const panel = this; panel.history && panel.history.redo(); }, async record() { const panel = this; const renderData = {}; for (const renderName in panel.contentRenders) { const { contentRender } = panel.contentRenders[renderName]; if (!Array.isArray(contentRender.__panels__)) { continue; } renderData[renderName] = []; for (let i = 0; i < contentRender.__panels__.length; i++) { try { if (contentRender.__panels__[i].panelObject.record) { const data = await contentRender.__panels__[i].callMethod('record'); renderData[renderName].push(data); } else { renderData[renderName].push(null); } } catch (error) { renderData[renderName].push(null); console.debug(error); } } } return { uuidListStr: JSON.stringify(panel.uuidList), metaListStr: JSON.stringify(panel.metaList), renderDataStr: JSON.stringify(renderData), }; }, restore(record) { const panel = this; try { const { uuidListStr, metaListStr, renderDataStr } = record; // uuid 数据不匹配表明不是同一个编辑对象了 if (JSON.stringify(panel.uuidList) !== uuidListStr) { return false; } // metaList 数据不一样的对 metaList 进行更新 if (JSON.stringify(panel.metaList) !== metaListStr) { panel.metaList = JSON.parse(metaListStr); for (const renderName in panel.contentRenders) { const { contentRender } = panel.contentRenders[renderName]; if (!Array.isArray(contentRender.__panels__)) { continue; } for (let i = 0; i < contentRender.__panels__.length; i++) { contentRender.__panels__[i].update(panel.assetList, panel.metaList); } } } const renderData = JSON.parse(renderDataStr); for (const renderName in panel.contentRenders) { const { contentRender } = panel.contentRenders[renderName]; if (!Array.isArray(contentRender.__panels__)) { continue; } if (!Array.isArray(renderData[renderName])) { continue; } if (!renderData[renderName].length) { continue; } for (let i = 0; i < contentRender.__panels__.length; i++) { if (renderData[renderName][i] && contentRender.__panels__[i].panelObject.restore) { contentRender.__panels__[i].callMethod('restore', renderData[renderName][i]); } } } Elements.header.isDirty.call(panel); return true; } catch (error) { console.error(error); return false; } }, async isDirty() { const panel = this; let isDirty = false; // 1/2 满足大部分资源的情况,因为大部分资源只修改 meta 数据 if (panel.metaList) { isDirty = panel.metaList.some((meta, index) => { return panel.metaListOrigin[index] !== JSON.stringify(meta); }); if (isDirty) { return isDirty; } } // 2/2 部分资源需要 scene 配合,数据的是否变动需要调接口 for (const renderName in panel.contentRenders) { const { contentRender } = panel.contentRenders[renderName]; if (!Array.isArray(contentRender.__panels__)) { continue; } for (let i = 0; i < contentRender.__panels__.length; i++) { isDirty = await contentRender.__panels__[i].callMethod('isDirty'); if (isDirty) { return isDirty; } } } return isDirty; }, async save() { const panel = this; // 首先调用所有 panel 里的 methods.canApply 检查是否允许保存 const tasks = []; for (const renderName in panel.contentRenders) { const { contentRender } = panel.contentRenders[renderName]; if (!Array.isArray(contentRender.__panels__)) { continue; } for (let i = 0; i < contentRender.__panels__.length; i++) { tasks.push(contentRender.__panels__[i].callMethod('canApply')); } } const canApplyResults = await Promise.all(tasks); const canApply = !canApplyResults.some((boolean) => { return boolean === false; }); // 不允许保存则中断 if (!canApply) { return; } // 有些资源在内部的 apply 保存数据后,会自动重导资源,自动更新 meta 数据,所以 meta 不需要再额外更新 let continueSaveMeta = true; for (const renderName in panel.contentRenders) { const { contentRender } = panel.contentRenders[renderName]; if (!Array.isArray(contentRender.__panels__)) { continue; } for (let i = 0; i < contentRender.__panels__.length; i++) { const saveState = await contentRender.__panels__[i].callMethod('apply'); /** * return false; 是保存失败 * return true; 是保存成功,但不继续保存 meta * return; 是保存成功,且向上冒泡继续保存 meta */ if (saveState === false) { return; } else if (saveState === true) { continueSaveMeta = false; } } } panel.$.header.removeAttribute('dirty'); if (continueSaveMeta === false) { return; } panel.uuidList.forEach((uuid, index) => { const content = JSON.stringify(panel.metaList[index]); // 没有变化则不修改 if (content === panel.metaListOrigin[index]) { return; } panel.metaListOrigin[index] = content; Editor.Message.request('asset-db', 'save-asset-meta', uuid, content); }); }, async abort() { const panel = this; panel.$.header.removeAttribute('dirty'); for (const renderName in panel.contentRenders) { const { contentRender } = panel.contentRenders[renderName]; for (let i = 0; i < contentRender.__panels__.length; i++) { await contentRender.__panels__[i].callMethod('abort'); } } }, async reset() { const panel = this; panel.$.header.removeAttribute('dirty'); for (const renderName in panel.contentRenders) { const { contentRender } = panel.contentRenders[renderName]; for (let i = 0; i < contentRender.__panels__.length; i++) { await contentRender.__panels__[i].callMethod('reset'); } } if (panel.ready !== true) { return; } panel.$this.update(panel.uuidList, panel.renderMap); }, setHelpUrl($link, data) { if (data) { $link.helpData = data; } else { if (!$link.helpData) { return; } data = $link.helpData; } const url = this.getHelpUrl(data); if (url) { $link.style.display = 'block'; $link.value = url; } else { $link.style.display = 'none'; } }, getHelpUrl(data) { return Editor.I18n.t(`ENGINE.help.assets.${data.help}`); }, replaceContainerWithUISection(params) { const panel = this; const $containerDiv = panel.$.container; const $header = panel.$.container.querySelector('.header'); $header.setAttribute('slot', 'header'); const $content = panel.$.container.querySelector('.content'); const $containerUISection = document.createElement('ui-section'); $containerUISection.setAttribute('class', 'container config no-padding'); $containerUISection.setAttribute('cache-expand', params.uuid); $containerUISection.appendChild($header); $containerUISection.appendChild($content); $containerDiv.replaceWith($containerUISection); }, }; exports.update = async function update(uuidList, renderMap, dropConfig) { const panel = this; const enginePath = path.join('editor', 'inspector', 'assets'); 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; for (const prop in Elements) { const element = Elements[prop]; if (element.update) { await element.update.call(panel); } } panel.history && panel.history.snapshot(panel); }; exports.ready = function ready() { const panel = this; panel.ready = true; for (const prop in Elements) { const element = Elements[prop]; if (element.ready) { element.ready.call(panel); } } }; exports.beforeClose = async function beforeClose() { const panel = this; if (panel.isDialoging) { return false; } for (const renderName in panel.contentRenders) { const { contentRender } = panel.contentRenders[renderName]; if (!Array.isArray(contentRender.__panels__)) { continue; } for (let i = 0; i < contentRender.__panels__.length; i++) { const canClose = await contentRender.__panels__[i].canClose(); if (!canClose) { return false; } } } const isDirty = await panel.isDirty(); if (!isDirty) { return true; } let result = 2; if (await Editor.Profile.getConfig('inspector', 'asset.auto_save')) { result = 1; } else { panel.isDialoging = true; const message = Editor.I18n.t(`ENGINE.assets.check_is_saved.assetMessage`).replace('${assetName}', panel.asset.name); const warnResult = await Editor.Dialog.warn(message, { buttons: [Editor.I18n.t('ENGINE.assets.check_is_saved.abort'), Editor.I18n.t('ENGINE.assets.check_is_saved.save'), 'Cancel'], default: 1, cancel: 2, }); result = warnResult.response; panel.isDialoging = false; } if (result === 0) { // abort await panel.abort(); return true; } if (result === 1) { // save await panel.save(); return true; } return false; }; exports.close = async function close() { const panel = this; panel.ready = false; for (const prop in Elements) { const element = Elements[prop]; if (element.close) { element.close.call(panel); } } }; exports.config = { header: require('../assets-header'), section: require('../assets'), footer: require('../assets-footer'), };