asset.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { injectionStyle } = require('../utils/prop');
  5. const History = require('./asset-history/index');
  6. exports.listeners = {};
  7. exports.style = fs.readFileSync(path.join(__dirname, './asset.css'), 'utf8');
  8. exports.template = `
  9. <div class="container">
  10. <header class="header">
  11. <ui-asset-image class="asset-thumbnail" size="small" tooltip="i18n:ENGINE.assets.locate_asset"></ui-asset-image>
  12. <ui-label class="name"></ui-label>
  13. <ui-button class="save tiny green" tooltip="i18n:ENGINE.assets.save">
  14. <ui-icon value="check"></ui-icon>
  15. </ui-button>
  16. <ui-button class="reset tiny" tooltip="i18n:ENGINE.assets.reset">
  17. <ui-icon value="reset" color></ui-icon>
  18. </ui-button>
  19. <ui-button class="location transparent" icon tooltip="i18n:ENGINE.assets.locate_asset">
  20. <ui-icon value="location"></ui-icon>
  21. </ui-button>
  22. <ui-button class="copy transparent" icon tooltip="i18n:ENGINE.inspector.cloneToEdit">
  23. <ui-icon value="copy"></ui-icon>
  24. </ui-button>
  25. <ui-link value="" class="help" tooltip="i18n:ENGINE.menu.help_url">
  26. <ui-icon value="help"></ui-icon>
  27. </ui-link>
  28. </header>
  29. <section class="content">
  30. <section class="content-header">
  31. <inspector-resize-preview area="header"></inspector-resize-preview>
  32. </section>
  33. <section class="content-section"></section>
  34. <section class="content-footer">
  35. <inspector-resize-preview area="footer"></inspector-resize-preview>
  36. </section>
  37. </section>
  38. </div>
  39. `;
  40. exports.$ = {
  41. container: '.container',
  42. header: '.header',
  43. content: '.content',
  44. location: '.location',
  45. copy: '.copy',
  46. assetThumbnail: '.asset-thumbnail',
  47. name: '.name',
  48. help: '.help',
  49. save: '.save',
  50. reset: '.reset',
  51. contentHeader: '.content-header',
  52. contentSection: '.content-section',
  53. contentFooter: '.content-footer',
  54. };
  55. const Elements = {
  56. panel: {
  57. ready() {
  58. const panel = this;
  59. panel.__assetChangedHandle__ = undefined;
  60. panel.__assetChanged__ = (uuid) => {
  61. if (Array.isArray(panel.uuidList) && panel.uuidList.includes(uuid)) {
  62. window.cancelAnimationFrame(panel.__assetChangedHandle__);
  63. panel.__assetChangedHandle__ = window.requestAnimationFrame(async () => {
  64. await panel.reset();
  65. });
  66. }
  67. };
  68. Editor.Message.addBroadcastListener('asset-db:asset-change', panel.__assetChanged__);
  69. panel.i18nChangeBind = Elements.panel.i18nChange.bind(panel);
  70. Editor.Message.addBroadcastListener('i18n:change', panel.i18nChangeBind);
  71. panel.history = new History();
  72. },
  73. async update() {
  74. const panel = this;
  75. let assetList = [];
  76. try {
  77. assetList = await Promise.all(
  78. panel.uuidList.map((uuid) => {
  79. return Editor.Message.request('asset-db', 'query-asset-info', uuid);
  80. }),
  81. );
  82. } catch (err) {
  83. console.error(err);
  84. }
  85. assetList = assetList.filter(Boolean);
  86. panel.asset = assetList[0];
  87. panel.assetList = [];
  88. panel.uuidList = [];
  89. panel.type = 'unknown';
  90. if (panel.asset) {
  91. // 以第一个资源的类型,过滤多选的其他不同资源; 过滤只读资源的多选
  92. const type = panel.asset.importer;
  93. assetList.forEach((asset) => {
  94. if (asset.importer === type) {
  95. if (panel.uuidList.length > 0 && asset.readonly) {
  96. return;
  97. }
  98. panel.uuidList.push(asset.uuid);
  99. panel.assetList.push(asset);
  100. }
  101. });
  102. }
  103. // 判断数据合法性
  104. if (!panel.asset) {
  105. panel.$.container.style.display = 'none';
  106. } else {
  107. panel.$.container.style.display = 'flex';
  108. panel.type = panel.asset.importer;
  109. if (panel.assetList.some((asset) => asset.importer !== panel.type)) {
  110. panel.type = 'unknown';
  111. }
  112. }
  113. panel.$this.setAttribute('sub-type', panel.type);
  114. if (panel.type === 'unknown') {
  115. panel.metaList = [];
  116. panel.metaListOrigin = [];
  117. return;
  118. }
  119. try {
  120. panel.metaList = await Promise.all(
  121. panel.uuidList.map((uuid) => {
  122. return Editor.Message.request('asset-db', 'query-asset-meta', uuid);
  123. }),
  124. );
  125. } catch (err) {
  126. console.error(err);
  127. panel.metaList = [];
  128. }
  129. panel.metaList = panel.metaList.filter(Boolean);
  130. panel.metaListOrigin = panel.metaList.map((meta) => {
  131. return JSON.stringify(meta);
  132. });
  133. panel.setHelpUrl(panel.$.help, { help: panel.type });
  134. },
  135. close() {
  136. const panel = this;
  137. if (panel.__assetChangedHandle__) {
  138. window.cancelAnimationFrame(panel.__assetChangedHandle__);
  139. panel.__assetChangedHandle__ = undefined;
  140. }
  141. Editor.Message.removeBroadcastListener('i18n:change', panel.i18nChangeBind);
  142. Editor.Message.removeBroadcastListener('asset-db:asset-change', panel.__assetChanged__);
  143. delete panel.history;
  144. },
  145. i18nChange() {
  146. const panel = this;
  147. const $links = panel.$.container.querySelectorAll('ui-link');
  148. $links.forEach($link => panel.setHelpUrl($link));
  149. },
  150. },
  151. header: {
  152. ready() {
  153. const panel = this;
  154. panel.$.save.addEventListener('click', (event) => {
  155. event.stopPropagation();
  156. panel.save();
  157. });
  158. panel.$.reset.addEventListener('click', (event) => {
  159. event.stopPropagation();
  160. panel.reset();
  161. });
  162. panel.$.copy.addEventListener('click', async (event) => {
  163. event.stopPropagation();
  164. const assetsDir = path.join(Editor.Project.path, 'assets');
  165. const result = await Editor.Dialog.select({
  166. path: assetsDir,
  167. type: 'directory',
  168. });
  169. let filePath = result.filePaths[0];
  170. if (!filePath) {
  171. return;
  172. }
  173. filePath = path.join(filePath, panel.asset.name);
  174. // 必须保存在 /assets 文件夹下
  175. if (!Editor.Utils.Path.contains(assetsDir, filePath)) {
  176. await Editor.Dialog.warn(Editor.I18n.t('ENGINE.dialog.warn'), {
  177. detail: Editor.I18n.t('ENGINE.inspector.cloneToDirectoryIllegal'),
  178. buttons: [Editor.I18n.t('ENGINE.dialog.confirm')],
  179. });
  180. return;
  181. }
  182. const target = await Editor.Message.request('asset-db', 'query-url', filePath);
  183. if (target) {
  184. const asset = await Editor.Message.request('asset-db', 'copy-asset', panel.asset.url, target);
  185. if (asset) {
  186. const lastSelectType = Editor.Selection.getLastSelectedType();
  187. if (lastSelectType === 'asset') {
  188. // 纯资源模式下
  189. Editor.Selection.clear(lastSelectType);
  190. Editor.Selection.select(lastSelectType, asset.uuid);
  191. } else if (lastSelectType === 'node') {
  192. // 节点里使用资源的情况下,如材质
  193. Editor.Message.broadcast('inspector:replace-asset-uuid-in-nodes', panel.asset.uuid, asset.uuid);
  194. }
  195. }
  196. }
  197. });
  198. panel.$.assetThumbnail.addEventListener('click', (event) => {
  199. event.stopPropagation();
  200. panel.uuidList.forEach((uuid) => {
  201. Editor.Message.request('assets', 'ui-kit:touch-asset', uuid);
  202. });
  203. });
  204. panel.$.location.addEventListener('click', (event) => {
  205. event.stopPropagation();
  206. panel.uuidList.forEach((uuid) => {
  207. Editor.Message.request('assets', 'ui-kit:touch-asset', uuid);
  208. });
  209. });
  210. },
  211. update() {
  212. const panel = this;
  213. if (!panel.asset) {
  214. return;
  215. }
  216. panel.$.name.value = panel.assetList.length === 1 ? panel.asset.name : `${panel.assetList.length} selections`;
  217. if (panel.asset.readonly) {
  218. panel.$.name.setAttribute('tooltip', 'i18n:inspector.asset.prohibitEditInternalAsset');
  219. panel.$.name.setAttribute('readonly', '');
  220. if (panel.asset.source && panel.asset.importer !== 'database') {
  221. panel.$.copy.style.display = 'inline-flex';
  222. } else {
  223. panel.$.copy.style.display = 'none';
  224. }
  225. } else {
  226. panel.$.name.removeAttribute('tooltip');
  227. panel.$.name.removeAttribute('readonly');
  228. panel.$.copy.style.display = 'none';
  229. }
  230. panel.$.assetThumbnail.value = panel.asset.uuid;
  231. },
  232. async isDirty() {
  233. const panel = this;
  234. const isDirty = await panel.isDirty();
  235. if (isDirty) {
  236. panel.$.header.setAttribute('dirty', '');
  237. } else {
  238. panel.$.header.removeAttribute('dirty');
  239. }
  240. },
  241. },
  242. content: {
  243. ready() {
  244. const panel = this;
  245. panel.contentRenders = {};
  246. },
  247. async update() {
  248. const panel = this;
  249. // 重置渲染对象
  250. panel.contentRenders = {
  251. header: {
  252. list: [],
  253. contentRender: panel.$.contentHeader,
  254. },
  255. section: {
  256. list: panel.renderMap.section['unknown'],
  257. contentRender: panel.$.contentSection,
  258. },
  259. footer: {
  260. list: [],
  261. contentRender: panel.$.contentFooter,
  262. },
  263. };
  264. for (const renderName in panel.renderMap) {
  265. if (panel.renderMap[renderName] && panel.renderMap[renderName][panel.type]) {
  266. panel.contentRenders[renderName].list = panel.renderMap[renderName][panel.type];
  267. }
  268. }
  269. for (const renderName in panel.contentRenders) {
  270. const { list, contentRender } = panel.contentRenders[renderName];
  271. contentRender.__panels__ = Array.from(contentRender.children).filter((el) => el.tagName === 'UI-PANEL');
  272. let i = 0;
  273. for (i; i < list.length; i++) {
  274. const file = list[i];
  275. if (!contentRender.__panels__[i]) {
  276. contentRender.__panels__[i] = document.createElement('ui-panel');
  277. contentRender.__panels__[i].injectionStyle(injectionStyle);
  278. contentRender.__panels__[i].addEventListener('change', () => {
  279. Elements.header.isDirty.call(panel);
  280. });
  281. contentRender.__panels__[i].addEventListener('snapshot', () => {
  282. panel.history && panel.history.snapshot(panel);
  283. });
  284. contentRender.appendChild(contentRender.__panels__[i]);
  285. }
  286. contentRender.__panels__[i].setAttribute('src', file);
  287. }
  288. // 清除尾部多余的节点
  289. for (i; i < contentRender.__panels__.length; i++) {
  290. contentRender.removeChild(contentRender.__panels__[i]);
  291. }
  292. try {
  293. await Promise.all(
  294. contentRender.__panels__.map(($panel) => {
  295. return $panel.update(panel.assetList, panel.metaList);
  296. }),
  297. );
  298. } catch (err) {
  299. console.error(err);
  300. }
  301. }
  302. },
  303. },
  304. };
  305. exports.methods = {
  306. undo() {
  307. const panel = this;
  308. panel.history && panel.history.undo();
  309. },
  310. redo() {
  311. const panel = this;
  312. panel.history && panel.history.redo();
  313. },
  314. async record() {
  315. const panel = this;
  316. const renderData = {};
  317. for (const renderName in panel.contentRenders) {
  318. const { contentRender } = panel.contentRenders[renderName];
  319. if (!Array.isArray(contentRender.__panels__)) {
  320. continue;
  321. }
  322. renderData[renderName] = [];
  323. for (let i = 0; i < contentRender.__panels__.length; i++) {
  324. try {
  325. if (contentRender.__panels__[i].panelObject.record) {
  326. const data = await contentRender.__panels__[i].callMethod('record');
  327. renderData[renderName].push(data);
  328. } else {
  329. renderData[renderName].push(null);
  330. }
  331. } catch (error) {
  332. renderData[renderName].push(null);
  333. console.debug(error);
  334. }
  335. }
  336. }
  337. return {
  338. uuidListStr: JSON.stringify(panel.uuidList),
  339. metaListStr: JSON.stringify(panel.metaList),
  340. renderDataStr: JSON.stringify(renderData),
  341. };
  342. },
  343. restore(record) {
  344. const panel = this;
  345. try {
  346. const { uuidListStr, metaListStr, renderDataStr } = record;
  347. // uuid 数据不匹配表明不是同一个编辑对象了
  348. if (JSON.stringify(panel.uuidList) !== uuidListStr) {
  349. return false;
  350. }
  351. // metaList 数据不一样的对 metaList 进行更新
  352. if (JSON.stringify(panel.metaList) !== metaListStr) {
  353. panel.metaList = JSON.parse(metaListStr);
  354. for (const renderName in panel.contentRenders) {
  355. const { contentRender } = panel.contentRenders[renderName];
  356. if (!Array.isArray(contentRender.__panels__)) {
  357. continue;
  358. }
  359. for (let i = 0; i < contentRender.__panels__.length; i++) {
  360. contentRender.__panels__[i].update(panel.assetList, panel.metaList);
  361. }
  362. }
  363. }
  364. const renderData = JSON.parse(renderDataStr);
  365. for (const renderName in panel.contentRenders) {
  366. const { contentRender } = panel.contentRenders[renderName];
  367. if (!Array.isArray(contentRender.__panels__)) {
  368. continue;
  369. }
  370. if (!Array.isArray(renderData[renderName])) {
  371. continue;
  372. }
  373. if (!renderData[renderName].length) {
  374. continue;
  375. }
  376. for (let i = 0; i < contentRender.__panels__.length; i++) {
  377. if (renderData[renderName][i] && contentRender.__panels__[i].panelObject.restore) {
  378. contentRender.__panels__[i].callMethod('restore', renderData[renderName][i]);
  379. }
  380. }
  381. }
  382. Elements.header.isDirty.call(panel);
  383. return true;
  384. } catch (error) {
  385. console.error(error);
  386. return false;
  387. }
  388. },
  389. async isDirty() {
  390. const panel = this;
  391. let isDirty = false;
  392. // 1/2 满足大部分资源的情况,因为大部分资源只修改 meta 数据
  393. if (panel.metaList) {
  394. isDirty = panel.metaList.some((meta, index) => {
  395. return panel.metaListOrigin[index] !== JSON.stringify(meta);
  396. });
  397. if (isDirty) {
  398. return isDirty;
  399. }
  400. }
  401. // 2/2 部分资源需要 scene 配合,数据的是否变动需要调接口
  402. for (const renderName in panel.contentRenders) {
  403. const { contentRender } = panel.contentRenders[renderName];
  404. if (!Array.isArray(contentRender.__panels__)) {
  405. continue;
  406. }
  407. for (let i = 0; i < contentRender.__panels__.length; i++) {
  408. isDirty = await contentRender.__panels__[i].callMethod('isDirty');
  409. if (isDirty) {
  410. return isDirty;
  411. }
  412. }
  413. }
  414. return isDirty;
  415. },
  416. async save() {
  417. const panel = this;
  418. // 首先调用所有 panel 里的 methods.canApply 检查是否允许保存
  419. const tasks = [];
  420. for (const renderName in panel.contentRenders) {
  421. const { contentRender } = panel.contentRenders[renderName];
  422. if (!Array.isArray(contentRender.__panels__)) {
  423. continue;
  424. }
  425. for (let i = 0; i < contentRender.__panels__.length; i++) {
  426. tasks.push(contentRender.__panels__[i].callMethod('canApply'));
  427. }
  428. }
  429. const canApplyResults = await Promise.all(tasks);
  430. const canApply = !canApplyResults.some((boolean) => {
  431. return boolean === false;
  432. });
  433. // 不允许保存则中断
  434. if (!canApply) {
  435. return;
  436. }
  437. // 有些资源在内部的 apply 保存数据后,会自动重导资源,自动更新 meta 数据,所以 meta 不需要再额外更新
  438. let continueSaveMeta = true;
  439. for (const renderName in panel.contentRenders) {
  440. const { contentRender } = panel.contentRenders[renderName];
  441. if (!Array.isArray(contentRender.__panels__)) {
  442. continue;
  443. }
  444. for (let i = 0; i < contentRender.__panels__.length; i++) {
  445. const saveState = await contentRender.__panels__[i].callMethod('apply');
  446. /**
  447. * return false; 是保存失败
  448. * return true; 是保存成功,但不继续保存 meta
  449. * return; 是保存成功,且向上冒泡继续保存 meta
  450. */
  451. if (saveState === false) {
  452. return;
  453. } else if (saveState === true) {
  454. continueSaveMeta = false;
  455. }
  456. }
  457. }
  458. panel.$.header.removeAttribute('dirty');
  459. if (continueSaveMeta === false) {
  460. return;
  461. }
  462. panel.uuidList.forEach((uuid, index) => {
  463. const content = JSON.stringify(panel.metaList[index]);
  464. // 没有变化则不修改
  465. if (content === panel.metaListOrigin[index]) {
  466. return;
  467. }
  468. panel.metaListOrigin[index] = content;
  469. Editor.Message.request('asset-db', 'save-asset-meta', uuid, content);
  470. });
  471. },
  472. async abort() {
  473. const panel = this;
  474. panel.$.header.removeAttribute('dirty');
  475. for (const renderName in panel.contentRenders) {
  476. const { contentRender } = panel.contentRenders[renderName];
  477. for (let i = 0; i < contentRender.__panels__.length; i++) {
  478. await contentRender.__panels__[i].callMethod('abort');
  479. }
  480. }
  481. },
  482. async reset() {
  483. const panel = this;
  484. panel.$.header.removeAttribute('dirty');
  485. for (const renderName in panel.contentRenders) {
  486. const { contentRender } = panel.contentRenders[renderName];
  487. for (let i = 0; i < contentRender.__panels__.length; i++) {
  488. await contentRender.__panels__[i].callMethod('reset');
  489. }
  490. }
  491. if (panel.ready !== true) {
  492. return;
  493. }
  494. panel.$this.update(panel.uuidList, panel.renderMap);
  495. },
  496. setHelpUrl($link, data) {
  497. if (data) {
  498. $link.helpData = data;
  499. } else {
  500. if (!$link.helpData) {
  501. return;
  502. }
  503. data = $link.helpData;
  504. }
  505. const url = this.getHelpUrl(data);
  506. if (url) {
  507. $link.style.display = 'block';
  508. $link.value = url;
  509. } else {
  510. $link.style.display = 'none';
  511. }
  512. },
  513. getHelpUrl(data) {
  514. return Editor.I18n.t(`ENGINE.help.assets.${data.help}`);
  515. },
  516. replaceContainerWithUISection(params) {
  517. const panel = this;
  518. const $containerDiv = panel.$.container;
  519. const $header = panel.$.container.querySelector('.header');
  520. $header.setAttribute('slot', 'header');
  521. const $content = panel.$.container.querySelector('.content');
  522. const $containerUISection = document.createElement('ui-section');
  523. $containerUISection.setAttribute('class', 'container config no-padding');
  524. $containerUISection.setAttribute('cache-expand', params.uuid);
  525. $containerUISection.appendChild($header);
  526. $containerUISection.appendChild($content);
  527. $containerDiv.replaceWith($containerUISection);
  528. },
  529. };
  530. exports.update = async function update(uuidList, renderMap, dropConfig) {
  531. const panel = this;
  532. const enginePath = path.join('editor', 'inspector', 'assets');
  533. Object.values(renderMap).forEach(config => {
  534. Object.values(config).forEach(renders => {
  535. renders.sort((a, b) => {
  536. return b.indexOf(enginePath) - a.indexOf(enginePath);
  537. });
  538. });
  539. });
  540. panel.uuidList = uuidList || [];
  541. panel.renderMap = renderMap;
  542. panel.dropConfig = dropConfig;
  543. for (const prop in Elements) {
  544. const element = Elements[prop];
  545. if (element.update) {
  546. await element.update.call(panel);
  547. }
  548. }
  549. panel.history && panel.history.snapshot(panel);
  550. };
  551. exports.ready = function ready() {
  552. const panel = this;
  553. panel.ready = true;
  554. for (const prop in Elements) {
  555. const element = Elements[prop];
  556. if (element.ready) {
  557. element.ready.call(panel);
  558. }
  559. }
  560. };
  561. exports.beforeClose = async function beforeClose() {
  562. const panel = this;
  563. if (panel.isDialoging) {
  564. return false;
  565. }
  566. for (const renderName in panel.contentRenders) {
  567. const { contentRender } = panel.contentRenders[renderName];
  568. if (!Array.isArray(contentRender.__panels__)) {
  569. continue;
  570. }
  571. for (let i = 0; i < contentRender.__panels__.length; i++) {
  572. const canClose = await contentRender.__panels__[i].canClose();
  573. if (!canClose) {
  574. return false;
  575. }
  576. }
  577. }
  578. const isDirty = await panel.isDirty();
  579. if (!isDirty) {
  580. return true;
  581. }
  582. let result = 2;
  583. if (await Editor.Profile.getConfig('inspector', 'asset.auto_save')) {
  584. result = 1;
  585. } else {
  586. panel.isDialoging = true;
  587. const message = Editor.I18n.t(`ENGINE.assets.check_is_saved.assetMessage`).replace('${assetName}', panel.asset.name);
  588. const warnResult = await Editor.Dialog.warn(message, {
  589. buttons: [Editor.I18n.t('ENGINE.assets.check_is_saved.abort'), Editor.I18n.t('ENGINE.assets.check_is_saved.save'), 'Cancel'],
  590. default: 1,
  591. cancel: 2,
  592. });
  593. result = warnResult.response;
  594. panel.isDialoging = false;
  595. }
  596. if (result === 0) {
  597. // abort
  598. await panel.abort();
  599. return true;
  600. }
  601. if (result === 1) {
  602. // save
  603. await panel.save();
  604. return true;
  605. }
  606. return false;
  607. };
  608. exports.close = async function close() {
  609. const panel = this;
  610. panel.ready = false;
  611. for (const prop in Elements) {
  612. const element = Elements[prop];
  613. if (element.close) {
  614. element.close.call(panel);
  615. }
  616. }
  617. };
  618. exports.config = {
  619. header: require('../assets-header'),
  620. section: require('../assets'),
  621. footer: require('../assets-footer'),
  622. };