node.js 93 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320
  1. 'use strict';
  2. const fs = require('fs');
  3. const path = require('path');
  4. module.paths.push(path.join(Editor.App.path, 'node_modules'));
  5. const { clipboard } = require('electron');
  6. const Profile = require('@base/electron-profile');
  7. const { throttle } = require('lodash');
  8. const utils = require('./utils');
  9. const { trackEventWithTimer } = require('../utils/metrics');
  10. const { injectionStyle, setLabel } = require('../utils/prop');
  11. // ipc messages protocol
  12. const messageProtocol = {
  13. scene: 'scene',
  14. };
  15. const lockList = [];
  16. let lockPerform = false;
  17. let lastSnapShot = null;
  18. async function performLock() {
  19. if (lockPerform) { return; }
  20. if (lockList.length === 0) { return; }
  21. lockPerform = true;
  22. const params = lockList.shift();
  23. const { snapshotLock, lock, uuids, cancel } = params;
  24. if (snapshotLock && !lock) {
  25. if (lastSnapShot) {
  26. await endRecording(lastSnapShot, cancel);
  27. lastSnapShot = null;
  28. }
  29. // start snapshot
  30. } else if (!snapshotLock && lock) {
  31. lastSnapShot = await beginRecording(uuids);
  32. }
  33. lockPerform = false;
  34. await performLock();
  35. }
  36. /**
  37. * 替换之前的snapshotLock,由于UI层的事件是同步的,
  38. * 而新的beginRecording是异步的,所以需要使用队列来保证顺序
  39. * @param {*} lock
  40. * @param {*} uuids
  41. * @param {*} cancel
  42. */
  43. function snapshotLock(panel, lock, uuids, cancel = false) {
  44. // 保存当前状态,放到队列中
  45. const params = {
  46. snapshotLock:panel.snapshotLock,
  47. lock,
  48. uuids,
  49. cancel,
  50. };
  51. lockList.push(params);
  52. // 执行队列中的操作;
  53. performLock();
  54. panel.snapshotLock = lock;
  55. }
  56. // 不传options时,会自动记录到undo队列,不需要调用endRecording
  57. async function beginRecording(uuids, options) {
  58. if (!uuids) { return; }
  59. const undoID = await Editor.Message.request(messageProtocol.scene, 'begin-recording', uuids, options);
  60. return undoID;
  61. }
  62. async function endRecording(undoID, cancel) {
  63. if (!undoID) { return; }
  64. if (cancel) {
  65. await Editor.Message.request(messageProtocol.scene, 'cancel-recording', undoID);
  66. } else {
  67. await Editor.Message.request(messageProtocol.scene, 'end-recording', undoID);
  68. }
  69. }
  70. exports.listeners = {
  71. async 'change-dump'(event) {
  72. const panel = this;
  73. const target = event.target;
  74. if (!target) {
  75. return;
  76. }
  77. clearTimeout(panel.previewTimeId);
  78. if (!panel.snapshotLock) {
  79. snapshotLock(panel, true, panel.uuidList);
  80. }
  81. const dump = target.dump;
  82. if (!dump || panel.isDialoging) {
  83. return;
  84. }
  85. let setChildrenLayer = false;
  86. if (dump.path === 'layer') {
  87. if (panel.dumps && panel.dumps.some((perdump) => perdump.children && perdump.children.length > 0)) {
  88. // 只修改自身节点
  89. let choose = 1;
  90. // 有子节点的时候才弹出对话框
  91. panel.isDialoging = true;
  92. const warnResult = await Editor.Dialog.warn(Editor.I18n.t(`ENGINE.components.layer.confirm_message`), {
  93. buttons: [
  94. Editor.I18n.t('ENGINE.components.layer.change_children'),
  95. Editor.I18n.t('ENGINE.components.layer.change_self'),
  96. 'Cancel',
  97. ],
  98. cancel: 2,
  99. });
  100. choose = warnResult.response;
  101. panel.isDialoging = false;
  102. // 取消,需要还原数值
  103. if (choose === 2) {
  104. dump.value = panel.$.nodeLayerSelect.prevValues[0];
  105. if (dump.values) {
  106. dump.values = panel.$.nodeLayerSelect.prevValues;
  107. }
  108. Elements.layer.update.call(panel);
  109. return;
  110. } else {
  111. setChildrenLayer = choose === 0 ? true : false;
  112. }
  113. }
  114. }
  115. try {
  116. panel.readyToUpdate = false;
  117. for (let i = 0; i < panel.uuidList.length; i++) {
  118. const uuid = panel.uuidList[i];
  119. const { path, type, isArray } = dump;
  120. let value = dump.value;
  121. if (dump.values) {
  122. value = dump.values[i];
  123. }
  124. if (setChildrenLayer) {
  125. await Editor.Message.request(messageProtocol.scene, 'set-node-and-children-layer', {
  126. uuid,
  127. dump: {
  128. value,
  129. },
  130. });
  131. continue;
  132. }
  133. await Editor.Message.request(messageProtocol.scene, 'set-property', {
  134. uuid,
  135. path,
  136. dump: {
  137. type,
  138. isArray,
  139. value,
  140. },
  141. });
  142. }
  143. } catch (error) {
  144. console.error(error);
  145. } finally {
  146. if (!panel.snapshotLock) {
  147. snapshotLock(panel, false);
  148. }
  149. panel.readyToUpdate = true;
  150. }
  151. },
  152. 'confirm-dump'() {
  153. const panel = this;
  154. clearTimeout(panel.previewTimeId);
  155. snapshotLock(panel, false);
  156. // In combination with change-dump, snapshot only generated once after ui-elements continuously changed.
  157. },
  158. async 'create-dump'(event) {
  159. const panel = this;
  160. const target = event.target;
  161. if (!target) {
  162. return;
  163. }
  164. clearTimeout(panel.previewTimeId);
  165. const undoID = await beginRecording(panel.uuidList);
  166. const dump = target.dump;
  167. let cancel = false;
  168. try {
  169. for (let i = 0; i < panel.uuidList.length; i++) {
  170. const uuid = panel.uuidList[i];
  171. if (i > 0) {
  172. dump.values[i] = dump.value;
  173. }
  174. await Editor.Message.request(messageProtocol.scene, 'update-property-from-null', {
  175. uuid,
  176. path: dump.path,
  177. });
  178. }
  179. } catch (error) {
  180. cancel = true;
  181. console.error(error);
  182. }
  183. await endRecording(undoID, cancel);
  184. },
  185. async 'reset-dump'(event) {
  186. const panel = this;
  187. const target = event.target;
  188. if (!target) {
  189. return;
  190. }
  191. clearTimeout(panel.previewTimeId);
  192. const undoID = await beginRecording(panel.uuidList);
  193. const dump = target.dump;
  194. try {
  195. for (let i = 0; i < panel.uuidList.length; i++) {
  196. const uuid = panel.uuidList[i];
  197. if (i > 0) {
  198. dump.values[i] = dump.value;
  199. }
  200. await Editor.Message.request(messageProtocol.scene, 'reset-property', {
  201. uuid,
  202. path: dump.path,
  203. });
  204. }
  205. } catch (error) {
  206. await endRecording(undoID, true);
  207. console.error(error);
  208. }
  209. },
  210. 'preview-dump'(event) {
  211. const panel = this;
  212. const target = event.target;
  213. if (!target) {
  214. return;
  215. }
  216. const dump = target.dump;
  217. if (!dump || panel.isDialoging) {
  218. return;
  219. }
  220. /**
  221. * Some assets don`t need to preview, like:
  222. * cc.AnimationClip
  223. */
  224. const notNeedToPreview = [
  225. 'cc.AnimationClip',
  226. ];
  227. if (notNeedToPreview.includes(dump.type)) {
  228. return;
  229. }
  230. const { method, value: assetUuid } = event.detail;
  231. clearTimeout(panel.previewTimeId);
  232. if (method === 'confirm') {
  233. try {
  234. panel.previewTimeId = setTimeout(() => {
  235. for (let i = 0; i < panel.uuidList.length; i++) {
  236. const uuid = panel.uuidList[i];
  237. const { path, type } = dump;
  238. let value = dump.value;
  239. if (dump.values) {
  240. value = dump.values[i];
  241. }
  242. // 预览新的值
  243. value.uuid = assetUuid;
  244. Editor.Message.send(messageProtocol.scene, 'preview-set-property', {
  245. uuid,
  246. path,
  247. dump: {
  248. type,
  249. value,
  250. },
  251. });
  252. }
  253. }, 500);
  254. } catch (error) {
  255. console.error(error);
  256. }
  257. } else if (method === 'cancel') {
  258. panel.previewTimeId = setTimeout(() => {
  259. try {
  260. for (let i = 0; i < panel.uuidList.length; i++) {
  261. const uuid = panel.uuidList[i];
  262. const { path } = dump;
  263. Editor.Message.send(messageProtocol.scene, 'cancel-preview-set-property', {
  264. uuid,
  265. path,
  266. });
  267. }
  268. } catch (error) {
  269. console.error(error);
  270. }
  271. }, 50);
  272. }
  273. },
  274. };
  275. exports.template = /* html*/`
  276. <ui-drag-area class="container">
  277. <header class="header">
  278. <section class="prefab" hidden>
  279. <ui-label value="Prefab"></ui-label>
  280. <ui-button role="edit" tooltip="i18n:ENGINE.prefab.edit">
  281. <ui-icon value="edit"></ui-icon>
  282. </ui-button>
  283. <div class="unlink-btns">
  284. <ui-button role="unlink" tooltip="i18n:ENGINE.prefab.unlink_tip">
  285. <ui-icon value="unlink"></ui-icon>
  286. </ui-button>
  287. <ui-button role="show-more">
  288. <ui-icon value="arrow-triangle"></ui-icon>
  289. </ui-button>
  290. </div>
  291. <ui-button role="local" tooltip="i18n:ENGINE.prefab.local">
  292. <ui-icon value="location"></ui-icon>
  293. </ui-button>
  294. <ui-button role="reset" tooltip="i18n:ENGINE.prefab.reset">
  295. <ui-icon value="reset"></ui-icon>
  296. </ui-button>
  297. <ui-button role="save" tooltip="i18n:ENGINE.prefab.save">
  298. <ui-icon value="save-o"></ui-icon>
  299. </ui-button>
  300. </section>
  301. <section class="node">
  302. <ui-checkbox class="active"></ui-checkbox>
  303. <ui-input class="name"></ui-input>
  304. </section>
  305. </header>
  306. <section class="body">
  307. <section class="component scene">
  308. <ui-prop class="release" type="dump"></ui-prop>
  309. <ui-prop class="ambient" type="dump" ui-section-config></ui-prop>
  310. <ui-section class="skybox config" expand cache-expand="inspector-scene-skybox">
  311. <div slot="header" class="component-header">
  312. <span>Skybox</span>
  313. <ui-link tooltip="i18n:scene.menu.help_url">
  314. <ui-icon value="help"></ui-icon>
  315. </ui-link>
  316. </div>
  317. <div class="before"></div>
  318. <ui-section class="envmap" expand cache-expand="inspector-scene-skybox-envmap">
  319. <ui-label slot="header" value="Envmap"></ui-label>
  320. <ui-radio-group class="useHDR" default-value="HDR" value="HDR">
  321. <ui-prop class="envmap-prop">
  322. <ui-radio class="envmap-radio" slot="label" type="single" value="HDR" tabindex="0">
  323. <ui-label value="HDR"></ui-label>
  324. </ui-radio>
  325. <ui-prop slot="content" class="envmapHDR" type="dump" no-label ui-section-config></ui-prop>
  326. </ui-prop>
  327. <ui-prop class="envmap-prop">
  328. <ui-radio class="envmap-radio" slot="label" type="single" value="LDR" tabindex="0">
  329. <ui-label value="LDR"></ui-label>
  330. </ui-radio>
  331. <ui-prop slot="content" class="envmapLDR" type="dump" no-label ui-section-config></ui-prop>
  332. </ui-prop>
  333. </ui-radio-group>
  334. <ui-prop class="reflection">
  335. <ui-label slot="label">Reflection Convolution</ui-label>
  336. <div slot="content">
  337. <ui-loading style="display:none; position: relative;top: 4px;"></ui-loading>
  338. <ui-button class="blue bake" style="display:none;">Bake</ui-button>
  339. <ui-button class="red remove" style="display:none;">Remove</ui-button>
  340. </div>
  341. </ui-prop>
  342. </ui-section>
  343. <div class="after"></div>
  344. </ui-section>
  345. <ui-prop class="fog" type="dump" ui-section-config></ui-prop>
  346. <ui-prop class="shadows" type="dump" ui-section-config></ui-prop>
  347. <ui-prop class="octree" type="dump" ui-section-config></ui-prop>
  348. <ui-prop class="skin" type="dump" ui-section-config></ui-prop>
  349. <ui-prop class="postSettings" type="dump" ui-section-config></ui-prop>
  350. </section>
  351. <ui-section class="component node config" expand cache-expand="inspector-node">
  352. <header class="component-header" slot="header">
  353. <span class="name">Node</span>
  354. <ui-link class="link" tooltip="i18n:ENGINE.menu.help_url">
  355. <ui-icon value="help"></ui-icon>
  356. </ui-link>
  357. <ui-icon class="menu" value="menu" tooltip="i18n:ENGINE.menu.component"></ui-icon>
  358. </header>
  359. <ui-prop class="position" type="dump"></ui-prop>
  360. <ui-prop class="rotation" type="dump"></ui-prop>
  361. <ui-prop class="scale" type="dump"></ui-prop>
  362. <ui-prop class="mobility" type="dump"></ui-prop>
  363. <ui-prop class="layer">
  364. <ui-label slot="label" value="Layer"></ui-label>
  365. <div class="layer-content" slot="content">
  366. <ui-prop class="layer-select" type="dump" no-label></ui-prop>
  367. <ui-button class="layer-edit">Edit</ui-button>
  368. </div>
  369. </ui-prop>
  370. <div class="node-section"></div>
  371. </ui-section>
  372. <section class="section-body"></section>
  373. <section class="section-missing"></section>
  374. <footer class="footer">
  375. <ui-button class="add-component" size="medium">
  376. <ui-label value="i18n:ENGINE.components.add_component"></ui-label>
  377. </ui-button>
  378. </footer>
  379. <section class="section-asset"></section>
  380. </section>
  381. </ui-drag-area>
  382. `;
  383. exports.style = fs.readFileSync(path.join(__dirname, './node.css'), 'utf8');
  384. exports.$ = {
  385. container: '.container',
  386. header: '.container > .header',
  387. body: '.container > .body',
  388. prefab: '.container > .header > .prefab',
  389. prefabUnlink: '.container > .header > .prefab [role="unlink"]',
  390. prefabMore: '.container > .header > .prefab [role="show-more"]',
  391. prefabLocal: '.container > .header > .prefab > [role="local"]',
  392. prefabReset: '.container > .header > .prefab > [role="reset"]',
  393. prefabSave: '.container > .header > .prefab > [role="save"]',
  394. prefabEdit: '.container > .header > .prefab > [role="edit"]',
  395. active: '.container > .header > .node > .active',
  396. name: '.container > .header > .node > .name',
  397. scene: '.container > .body > .scene',
  398. sceneRelease: '.container > .body > .scene > .release',
  399. sceneAmbient: '.container > .body > .scene > .ambient',
  400. sceneFog: '.container > .body > .scene > .fog',
  401. sceneShadows: '.container > .body > .scene > .shadows',
  402. sceneSkybox: '.container > .body > .scene > .skybox',
  403. sceneSkyboxBefore: '.container > .body > .scene > .skybox > .before',
  404. sceneSkyboxUseHDR: '.container > .body > .scene > .skybox .useHDR',
  405. sceneSkyboxEnvmapHDR: '.container > .body > .scene > .skybox .envmapHDR',
  406. sceneSkyboxEnvmapLDR: '.container > .body > .scene > .skybox .envmapLDR',
  407. sceneSkyboxReflection: '.container > .body > .scene > .skybox .reflection',
  408. sceneSkyboxReflectionLoading: '.container > .body > .scene > .skybox .reflection ui-loading',
  409. sceneSkyboxReflectionBake: '.container > .body > .scene > .skybox .reflection .bake',
  410. sceneSkyboxReflectionRemove: '.container > .body > .scene > .skybox .reflection .remove',
  411. sceneSkyboxAfter: '.container > .body > .scene > .skybox > .after',
  412. sceneOctree: '.container > .body > .scene > .octree',
  413. sceneSkin: '.container > .body > .scene > .skin',
  414. scenePostSettings: '.container > .body > .scene > .postSettings',
  415. node: '.container > .body > .node',
  416. nodeHeader: '.container > .body > .node > .component-header',
  417. nodeSection: '.container > .body > .node >.node-section',
  418. nodeMenu: '.container > .body > .node > .component-header > .menu',
  419. nodeLink: '.container > .body > .node > .component-header > .link',
  420. nodePosition: '.container > .body > .node > .position',
  421. nodeRotation: '.container > .body > .node > .rotation',
  422. nodeScale: '.container > .body > .node > .scale',
  423. nodeMobility: '.container > .body > .node > .mobility',
  424. nodeLayer: '.container > .body > .node > .layer > ui-label',
  425. nodeLayerSelect: '.container > .body > .node > .layer .layer-select',
  426. nodeLayerButton: '.container > .body > .node > .layer .layer-edit',
  427. sectionBody: '.container > .body > .section-body',
  428. sectionMissing: '.container > .body > .section-missing',
  429. sectionAsset: '.container > .body > .section-asset',
  430. footer: '.container > .body > .footer',
  431. componentAdd: '.container > .body > .footer .add-component',
  432. };
  433. const Elements = {
  434. panel: {
  435. ready() {
  436. const panel = this;
  437. panel.throttleUpdate = throttle(async () => {
  438. if (!panel.readyToUpdate) {
  439. return;
  440. }
  441. for (const prop in Elements) {
  442. const element = Elements[prop];
  443. if (element.update) {
  444. await element.update.call(panel);
  445. }
  446. }
  447. }, 100, { leading: false, trailing: true });
  448. panel.__nodeChanged__ = (uuid) => {
  449. if (panel.throttleUpdate && Array.isArray(panel.uuidList) && panel.uuidList.includes(uuid)) {
  450. panel.throttleUpdate();
  451. }
  452. };
  453. Editor.Message.addBroadcastListener('scene:change-node', panel.__nodeChanged__);
  454. panel.__animationTimeChange__ = () => {
  455. if (!panel.isAnimationMode()) {
  456. return;
  457. }
  458. panel.__nodeChanged__(panel.uuidList[0]);
  459. };
  460. Editor.Message.addBroadcastListener('scene:animation-time-change', panel.__animationTimeChange__);
  461. panel.__projectSettingChanged__ = async function(name) {
  462. if (name !== 'layers') {
  463. return;
  464. }
  465. for (const prop in Elements) {
  466. const element = Elements[prop];
  467. if (element.update) {
  468. await element.update.call(panel);
  469. }
  470. }
  471. };
  472. Editor.Message.addBroadcastListener('project:setting-change', panel.__projectSettingChanged__);
  473. panel.__throttleProfileChanged__ = throttle(async (protocol, file, key) => {
  474. if (protocol === 'defaultPreferences' && file === 'packages/inspector.json' && key === 'message-protocol') {
  475. await panel.__queryMessageProtocolScene__();
  476. for (const prop in Elements) {
  477. const element = Elements[prop];
  478. if (element.update) {
  479. await element.update.call(panel);
  480. }
  481. }
  482. }
  483. }, 100, { leading: false, trailing: true });
  484. if (panel.__throttleProfileChanged__) {
  485. Profile.on('change', panel.__throttleProfileChanged__);
  486. }
  487. // 识别拖入脚本资源
  488. panel.$.container.addEventListener('dragover', (event) => {
  489. event.preventDefault();
  490. if (panel.dump.isScene) {
  491. event.dataTransfer.dropEffect = 'none';
  492. } else {
  493. event.dataTransfer.dropEffect = 'copy';
  494. }
  495. });
  496. panel.$.container.addEventListener('drop', async (event) => {
  497. event.preventDefault();
  498. event.stopPropagation();
  499. if (panel.dump.isScene) {
  500. return;
  501. }
  502. // 支持多选脚本拖入
  503. const { additional = [], value, type } = JSON.parse(JSON.stringify(Editor.UI.DragArea.currentDragInfo)) || {};
  504. if (value && additional.every((v) => v.value !== value)) {
  505. additional.push({ value, type });
  506. }
  507. const undoID = await Editor.Message.request('scene', 'begin-recording', panel.uuidList);
  508. for (const info of additional) {
  509. const config = panel.dropConfig[info.type];
  510. if (config) {
  511. await Editor.Message.request(config.package, config.message, info, panel.dumps, panel.uuidList);
  512. }
  513. }
  514. await Editor.Message.request('scene', 'end-recording', undoID);
  515. });
  516. panel._readyToUpdate = true;
  517. if (panel.readyToUpdate === undefined) {
  518. Object.defineProperty(panel, 'readyToUpdate', {
  519. enumerable: true,
  520. get() {
  521. return panel._readyToUpdate;
  522. },
  523. set(val) {
  524. panel._readyToUpdate = val;
  525. if (val && panel.throttleUpdate) {
  526. panel.throttleUpdate();
  527. }
  528. },
  529. });
  530. }
  531. },
  532. async update() {
  533. const panel = this;
  534. let dumps = [];
  535. try {
  536. dumps = await Promise.all(
  537. panel.uuidList.map((uuid) => {
  538. return Editor.Message.request(messageProtocol.scene, 'query-node', uuid);
  539. }),
  540. );
  541. } catch (err) {
  542. console.error(err);
  543. }
  544. dumps = dumps.filter(Boolean);
  545. panel.dump = dumps[0];
  546. panel.dumps = [];
  547. panel.uuidList = [];
  548. panel.assets = {};
  549. if (panel.dump) {
  550. panel.$.container.removeAttribute('hidden');
  551. // 以第一个节点的类型,过滤多选的其他不同类型,比如 node 和 sceneNode 就不能混为多选编辑
  552. const type = panel.dump.__type__;
  553. dumps.forEach((dump) => {
  554. if (dump.__type__ === type) {
  555. panel.uuidList.push(dump.uuid.value);
  556. panel.dumps.push(dump);
  557. }
  558. });
  559. // 补充缺失的 dump 数据,如 path values 等,收集节点内的资源
  560. utils.translationDump(panel.dump, panel.dumps.length > 1 ? panel.dumps : undefined, panel.assets);
  561. } else {
  562. panel.$.container.setAttribute('hidden', '');
  563. }
  564. },
  565. close() {
  566. const panel = this;
  567. if (panel.throttleUpdate) {
  568. panel.throttleUpdate.cancel();
  569. }
  570. panel.throttleUpdate = undefined;
  571. Editor.Message.removeBroadcastListener('scene:change-node', panel.__nodeChanged__);
  572. Editor.Message.removeBroadcastListener('scene:animation-time-change', panel.__animationTimeChange__);
  573. Editor.Message.removeBroadcastListener('project:setting-change', panel.__projectSettingChanged__);
  574. if (panel.__throttleProfileChanged__) {
  575. Profile.removeListener('change', panel.__throttleProfileChanged__);
  576. panel.__throttleProfileChanged__.cancel();
  577. }
  578. panel.__throttleProfileChanged__ = undefined;
  579. },
  580. },
  581. prefab: {
  582. ready() {
  583. const panel = this;
  584. panel.$.prefab.addEventListener('confirm', async (event) => {
  585. const button = event.target;
  586. if (!panel.dump || !panel.dump.__prefab__ || !button || !panel.dumps?.length) {
  587. return;
  588. }
  589. const role = button.getAttribute('role');
  590. for (const dump of panel.dumps) {
  591. const prefab = dump.__prefab__;
  592. switch (role) {
  593. case 'edit': {
  594. const assetId = prefab.prefabStateInfo?.assetUuid;
  595. if (!assetId) {
  596. return;
  597. }
  598. Editor.Message.request('asset-db', 'open-asset', assetId);
  599. break;
  600. }
  601. case 'unlink': {
  602. await Editor.Message.request(messageProtocol.scene, 'unlink-prefab', prefab.rootUuid, false);
  603. break;
  604. }
  605. case 'local': {
  606. Editor.Message.send('assets', 'twinkle', prefab.uuid);
  607. break;
  608. }
  609. case 'reset': {
  610. await Editor.Message.request(messageProtocol.scene, 'restore-prefab', prefab.rootUuid, prefab.uuid);
  611. break;
  612. }
  613. case 'save': {
  614. // apply-prefab是自定义的undo,在场景中实现了undo
  615. await Editor.Message.request(messageProtocol.scene, 'apply-prefab', prefab.rootUuid);
  616. break;
  617. }
  618. case 'show-more': {
  619. Editor.Menu.popup({
  620. menu: [
  621. {
  622. label: Editor.I18n.t('ENGINE.prefab.unlink'),
  623. async click() {
  624. await Editor.Message.request(messageProtocol.scene, 'unlink-prefab', prefab.rootUuid, false);
  625. },
  626. },
  627. {
  628. label: Editor.I18n.t('ENGINE.prefab.unlink_recursively'),
  629. async click() {
  630. await Editor.Message.send('hierarchy', 'unlink-prefab-recursively');
  631. },
  632. },
  633. ],
  634. });
  635. break;
  636. }
  637. }
  638. }
  639. });
  640. },
  641. async update() {
  642. const panel = this;
  643. if (!panel.dump || !panel.dump.__prefab__) {
  644. panel.$.prefab.setAttribute('hidden', '');
  645. return;
  646. }
  647. panel.$.prefab.removeAttribute('hidden');
  648. const prefab = panel.dump.__prefab__;
  649. const prefabStateInfo = prefab.prefabStateInfo;
  650. const canUnlink = panel.dumps.some(dump => {
  651. if (dump.__prefab__ && dump.__prefab__.prefabStateInfo) {
  652. const state = dump.__prefab__.prefabStateInfo.state;
  653. if (state === 2 || state === 3) {
  654. return true;
  655. }
  656. }
  657. return false;
  658. });
  659. if (canUnlink) {
  660. panel.$.prefabUnlink.removeAttribute('disabled');
  661. panel.$.prefabMore.removeAttribute('disabled');
  662. } else {
  663. panel.$.prefabUnlink.setAttribute('disabled', '');
  664. panel.$.prefabMore.setAttribute('disabled', '');
  665. }
  666. const assetInfo = await Editor.Message.request('asset-db', 'query-asset-info', prefab.uuid);
  667. if (assetInfo) {
  668. panel.$.prefab.removeAttribute('missing');
  669. if (prefabStateInfo.isRevertable) {
  670. panel.$.prefabReset.removeAttribute('disabled');
  671. } else {
  672. panel.$.prefabReset.setAttribute('disabled', '');
  673. }
  674. if (prefabStateInfo.isApplicable) {
  675. panel.$.prefabSave.removeAttribute('disabled');
  676. } else {
  677. panel.$.prefabSave.setAttribute('disabled', '');
  678. }
  679. if (assetInfo.uuid.includes('@')) {
  680. panel.$.prefabEdit.setAttribute('disabled', '');
  681. } else {
  682. panel.$.prefabEdit.removeAttribute('disabled');
  683. }
  684. } else {
  685. panel.$.prefab.setAttribute('missing', '');
  686. panel.$.prefabEdit.setAttribute('disabled', '');
  687. panel.$.prefabLocal.setAttribute('disabled', '');
  688. panel.$.prefabReset.setAttribute('disabled', '');
  689. panel.$.prefabSave.setAttribute('disabled', '');
  690. }
  691. if ((panel.dumps && panel.dumps.length > 1) || (Editor.EditMode.getMode() === 'prefab' && !prefabStateInfo.isUnwrappable)) {
  692. panel.$.prefabEdit.setAttribute('disabled', '');
  693. }
  694. },
  695. },
  696. header: {
  697. ready() {
  698. const panel = this;
  699. panel.$.active.addEventListener('change', (event) => {
  700. const value = event.target.value;
  701. const dump = event.target.dump;
  702. dump.value = value;
  703. if ('values' in dump) {
  704. dump.values.forEach((val, index) => {
  705. dump.values[index] = value;
  706. });
  707. }
  708. panel.$.active.dispatch('change-dump');
  709. });
  710. panel.$.active.addEventListener('confirm', () => {
  711. panel.$.active.dispatch('confirm-dump');
  712. });
  713. panel.$.name.addEventListener('change', (event) => {
  714. const value = event.target.value;
  715. const dump = event.target.dump;
  716. dump.value = value;
  717. if ('values' in dump) {
  718. dump.values.forEach((val, index) => {
  719. dump.values[index] = value;
  720. });
  721. }
  722. panel.$.name.dispatch('change-dump');
  723. });
  724. panel.$.name.addEventListener('confirm', () => {
  725. panel.$.name.dispatch('confirm-dump');
  726. });
  727. },
  728. update() {
  729. const panel = this;
  730. if (!panel.dump) {
  731. return;
  732. }
  733. let activeDisabled = false;
  734. let activeInvalid = false;
  735. let nameDisabled = false;
  736. let nameInvalid = false;
  737. if (panel.dump.isScene) {
  738. activeDisabled = true;
  739. nameDisabled = true;
  740. } else {
  741. if (panel.dumps && panel.dumps.length > 1) {
  742. // when changing, stop validating
  743. if (!panel.$.active.hasAttribute('focused')) {
  744. if (panel.dumps.some((dump) => dump.active.value !== panel.dump.active.value)) {
  745. activeInvalid = true;
  746. }
  747. }
  748. // when changing, stop validating
  749. if (!panel.$.name.hasAttribute('focused')) {
  750. if (panel.dumps.some((dump) => dump.name.value !== panel.dump.name.value)) {
  751. nameInvalid = true;
  752. }
  753. }
  754. }
  755. }
  756. panel.$.active.value = panel.dump.active.value;
  757. panel.$.active.dump = panel.dump.active;
  758. panel.$.active.disabled = activeDisabled;
  759. panel.$.active.invalid = activeInvalid;
  760. panel.$.name.value = panel.dump.name.value;
  761. panel.$.name.dump = panel.dump.name;
  762. panel.$.name.disabled = nameDisabled;
  763. panel.$.name.invalid = nameInvalid;
  764. },
  765. },
  766. scene: {
  767. ready() {
  768. const panel = this;
  769. const $help = panel.$.sceneSkybox.querySelector('ui-link');
  770. panel.setHelpUrl($help, { help: 'i18n:cc.Skybox' });
  771. $help.addEventListener('click', (event) => {
  772. event.stopPropagation();
  773. event.preventDefault();
  774. });
  775. panel.$.sceneSkyboxUseHDR.addEventListener('change', Elements.scene.skyboxUseHDRChange.bind(panel));
  776. panel.$.sceneSkyboxEnvmapHDR.addEventListener('change-dump', Elements.scene.skyboxEnvmapChange.bind(panel, true));
  777. panel.$.sceneSkyboxEnvmapLDR.addEventListener('change-dump', Elements.scene.skyboxEnvmapChange.bind(panel, false));
  778. panel.$.sceneSkyboxReflectionBake.addEventListener('confirm', Elements.scene.skyboxReflectionConvolutionBake.bind(panel));
  779. panel.$.sceneSkyboxReflectionRemove.addEventListener('confirm', Elements.scene.skyboxReflectionConvolutionRemove.bind(panel));
  780. },
  781. async update() {
  782. const panel = this;
  783. if (!panel.dump || !panel.dump.isScene) {
  784. if (!panel.isAnimationMode()) {
  785. panel.toggleShowAddComponentBtn(true);
  786. }
  787. return;
  788. }
  789. // 场景模式要隐藏按钮
  790. panel.toggleShowAddComponentBtn(false);
  791. panel.$this.setAttribute('sub-type', 'scene');
  792. panel.$.container.removeAttribute('droppable');
  793. panel.$.sceneRelease.render(panel.dump.autoReleaseAssets);
  794. // 由于场景属性对象不是继承于 Component 所以没有修饰器,displayName, help 数据在这里配置
  795. panel.dump._globals.ambient.displayName = 'Ambient';
  796. panel.dump._globals.ambient.editor = { help: 'i18n:cc.Ambient' };
  797. panel.dump._globals.ambient.help = panel.getHelpUrl(panel.dump._globals.ambient.editor);
  798. panel.$.sceneAmbient.render(panel.dump._globals.ambient);
  799. panel.dump._globals.fog.displayName = 'Fog';
  800. panel.dump._globals.fog.editor = { help: 'i18n:cc.Fog' };
  801. panel.dump._globals.fog.help = panel.getHelpUrl(panel.dump._globals.fog.editor);
  802. panel.$.sceneFog.render(panel.dump._globals.fog);
  803. panel.dump._globals.shadows.displayName = 'Shadows';
  804. panel.dump._globals.shadows.editor = { help: 'i18n:cc.Shadow' };
  805. panel.dump._globals.shadows.help = panel.getHelpUrl(panel.dump._globals.shadows.editor);
  806. panel.$.sceneShadows.render(panel.dump._globals.shadows);
  807. // skyBox 逻辑 start
  808. let $sceneSkyboxContainer = panel.$.sceneSkyboxBefore;
  809. const oldSkyboxProps = Object.keys(panel.$skyboxProps);
  810. const newSkyboxProps = [];
  811. // these properties have custom editing interface
  812. const customProperties = ['envmap', 'useHDR', '_envmapHDR', '_envmapLDR'];
  813. const afterPositionProperties = ['reflectionMap', 'diffuseMap'];
  814. for (const key in panel.dump._globals.skybox.value) {
  815. const dump = panel.dump._globals.skybox.value[key];
  816. if (customProperties.includes(key)) {
  817. if (key === 'useHDR') {
  818. panel.$.sceneSkyboxUseHDR.value = dump.value ? 'HDR' : 'LDR';
  819. panel.$.sceneSkyboxUseHDR.dump = dump;
  820. } else if (key === '_envmapHDR') {
  821. panel.$.sceneSkyboxEnvmapHDR.render(dump);
  822. } else if (key === '_envmapLDR') {
  823. panel.$.sceneSkyboxEnvmapLDR.render(dump);
  824. }
  825. continue;
  826. }
  827. if (!dump.visible) {
  828. continue;
  829. }
  830. const id = `${dump.type || dump.name}:${dump.path}`;
  831. let $prop = panel.$skyboxProps[id];
  832. newSkyboxProps.push(id);
  833. if (afterPositionProperties.includes(key)) {
  834. $sceneSkyboxContainer = panel.$.sceneSkyboxAfter;
  835. } else {
  836. $sceneSkyboxContainer = panel.$.sceneSkyboxBefore;
  837. }
  838. if (!$prop) {
  839. $prop = document.createElement('ui-prop');
  840. $prop.setAttribute('type', 'dump');
  841. panel.$skyboxProps[id] = $prop;
  842. $sceneSkyboxContainer.appendChild($prop);
  843. } else if (!$prop.isConnected || !$prop.parentElement) {
  844. $sceneSkyboxContainer.appendChild($prop);
  845. }
  846. $prop.render(dump);
  847. }
  848. for (const id of oldSkyboxProps) {
  849. if (!newSkyboxProps.includes(id)) {
  850. const $prop = panel.$skyboxProps[id];
  851. if ($prop && $prop.parentElement) {
  852. $prop.parentElement.removeChild($prop);
  853. }
  854. }
  855. }
  856. Elements.scene.skyboxReflectionConvolution.call(panel);
  857. // skyBox 逻辑 end
  858. panel.dump._globals.octree.displayName = 'Octree Scene Culling';
  859. panel.dump._globals.octree.editor = { help: 'i18n:cc.OctreeCulling' };
  860. panel.dump._globals.octree.help = panel.getHelpUrl(panel.dump._globals.octree.editor);
  861. panel.$.sceneOctree.render(panel.dump._globals.octree);
  862. panel.dump._globals.skin.displayName = 'Skin';
  863. panel.dump._globals.skin.editor = { help: 'i18n:cc.Skin' };
  864. panel.dump._globals.skin.help = panel.getHelpUrl(panel.dump._globals.skin.editor);
  865. panel.$.sceneSkin.render(panel.dump._globals.skin);
  866. panel.dump._globals.postSettings.displayName = 'PostSettings';
  867. panel.$.scenePostSettings.render(panel.dump._globals.postSettings);
  868. const $skyProps = panel.$.sceneSkybox.querySelectorAll('ui-prop[type="dump"]');
  869. $skyProps.forEach(($prop) => {
  870. if ($prop.dump.name === 'envLightingType') {
  871. if (!$prop.regenerate) {
  872. $prop.regenerate = Elements.scene.regenerate.bind(panel);
  873. $prop.addEventListener('change-dump', $prop.regenerate);
  874. }
  875. }
  876. });
  877. },
  878. async regenerate() {
  879. const panel = this;
  880. const dump = panel.dump._globals.skybox.value;
  881. if (!dump.envmap.value) {
  882. return;
  883. }
  884. const envMapUuid = dump.envmap.value.uuid;
  885. if (!envMapUuid) {
  886. return;
  887. }
  888. const envLightingType = dump.envLightingType.value;
  889. // DIFFUSEMAP_WITH_REFLECTION 的枚举值为 2
  890. if (envLightingType === 2) {
  891. await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
  892. name: 'inspector',
  893. method: 'generateDiffuseMap',
  894. args: [envMapUuid],
  895. });
  896. } else {
  897. await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
  898. name: 'inspector',
  899. method: 'generateVector',
  900. args: [envMapUuid],
  901. });
  902. }
  903. },
  904. async setEnvMapAndConvolutionMap(uuid) {
  905. await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
  906. name: 'inspector',
  907. method: 'setSkyboxEnvMap',
  908. args: [uuid],
  909. });
  910. if (uuid) {
  911. await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
  912. name: 'inspector',
  913. method: 'setReflectionConvolutionMap',
  914. args: [uuid],
  915. });
  916. }
  917. },
  918. async skyboxReflectionConvolution() {
  919. const panel = this;
  920. panel.$.sceneSkyboxReflection.style.display = 'inline-block';
  921. panel.$.sceneSkyboxReflectionLoading.style.display = 'none';
  922. const reflectionMap = panel.dump._globals.skybox.value['reflectionMap'];
  923. if (reflectionMap.value && reflectionMap.value.uuid) {
  924. panel.$.sceneSkyboxReflectionBake.style.display = 'none';
  925. panel.$.sceneSkyboxReflectionRemove.style.display = 'inline-flex';
  926. } else {
  927. panel.$.sceneSkyboxReflectionBake.style.display = 'inline-flex';
  928. panel.$.sceneSkyboxReflectionRemove.style.display = 'none';
  929. // if envmap value unexist, the column of bake button hidden;
  930. const envMapData = panel.dump._globals.skybox.value['envmap'];
  931. if (!envMapData.value || !envMapData.value.uuid) {
  932. panel.$.sceneSkyboxReflection.style.display = 'none';
  933. }
  934. }
  935. },
  936. async skyboxReflectionConvolutionBake() {
  937. const panel = this;
  938. const envMapData = panel.dump._globals.skybox.value['envmap'];
  939. if (!envMapData.value || !envMapData.value.uuid) {
  940. return;
  941. }
  942. panel.$.sceneSkyboxReflectionLoading.style.display = 'inline-flex';
  943. panel.$.sceneSkyboxReflectionBake.style.display = 'none';
  944. await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
  945. name: 'inspector',
  946. method: 'bakeReflectionConvolution',
  947. args: [envMapData.value.uuid],
  948. });
  949. },
  950. async skyboxReflectionConvolutionRemove() {
  951. const panel = this;
  952. const reflectionMap = panel.dump._globals.skybox.value['reflectionMap'];
  953. if (reflectionMap.value && reflectionMap.value.uuid) {
  954. const $skyProps = panel.$.sceneSkybox.querySelectorAll('ui-prop[type="dump"]');
  955. for (const $skyProp of $skyProps) {
  956. if ($skyProp.dump.name === 'reflectionMap') {
  957. const textCubeAssetUuid = $skyProp.dump.value.uuid;
  958. if (textCubeAssetUuid) {
  959. // remove asset
  960. try {
  961. const imageAssetUuid = textCubeAssetUuid.split('@')[0];
  962. const imageAssetUrl = await Editor.Message.request('asset-db', 'query-url', imageAssetUuid);
  963. if (imageAssetUrl) {
  964. await Editor.Message.request('asset-db', 'delete-asset', imageAssetUrl);
  965. }
  966. } catch (error) {
  967. console.error(error);
  968. }
  969. $skyProp.dump.value.uuid = '';
  970. $skyProp.dispatch('change-dump');
  971. $skyProp.dispatch('confirm-dump'); // for scene snapshot
  972. }
  973. break;
  974. }
  975. }
  976. }
  977. },
  978. skyboxUseHDRChange(event) {
  979. const panel = this;
  980. const $radioGraph = event.currentTarget;
  981. const useHDR = $radioGraph.value === 'HDR';
  982. $radioGraph.dump.value = useHDR;
  983. $radioGraph.dispatch('change-dump');
  984. $radioGraph.dispatch('confirm-dump'); // for scene snapshot
  985. const $prop = useHDR ? panel.$.sceneSkyboxEnvmapHDR : panel.$.sceneSkyboxEnvmapLDR;
  986. const uuid = $prop.dump.value.uuid;
  987. Elements.scene.setEnvMapAndConvolutionMap.call(panel, uuid);
  988. panel.$.scenePostSettings.style.display = useHDR ? 'block' : 'none';
  989. },
  990. skyboxEnvmapChange(useHDR, event) {
  991. const panel = this;
  992. if (panel.dump._globals.skybox.value['useHDR'].value !== useHDR) {
  993. // 未选中项的变动,不需要后续执行
  994. return;
  995. }
  996. const $prop = event.currentTarget;
  997. const uuid = $prop.dump.value.uuid;
  998. Elements.scene.setEnvMapAndConvolutionMap.call(panel, uuid);
  999. },
  1000. },
  1001. node: {
  1002. ready() {
  1003. const panel = this;
  1004. panel.$skyboxProps = {};
  1005. panel.setHelpUrl(panel.$.nodeLink, { help: 'i18n:cc.Node' });
  1006. panel.$.nodeMenu.addEventListener('click', (event) => {
  1007. event.stopPropagation();
  1008. exports.methods.nodeContextMenu(panel.uuidList, panel.dumps);
  1009. });
  1010. panel.$.nodeLink.addEventListener('click', (event) => {
  1011. event.stopPropagation();
  1012. });
  1013. panel.i18nChangeBind = Elements.node.i18nChange.bind(panel);
  1014. Editor.Message.addBroadcastListener('i18n:change', panel.i18nChangeBind);
  1015. // 针对layer节点属性的右键菜单
  1016. panel.$.nodeLayer && panel.$.nodeLayer.addEventListener('contextmenu', (event) => {
  1017. event.stopPropagation();
  1018. event.preventDefault();
  1019. if (!panel.dump || !panel.dump.layer) { return; }
  1020. const layer = panel.dump.layer;
  1021. const store = Elements.node.getAndParseClipboard();
  1022. const pasteEnable = Elements.node.validatePasteEnable(layer, store);
  1023. Editor.Menu.popup({
  1024. menu: [
  1025. {
  1026. label: Editor.I18n.t('ENGINE.menu.copy_property_path'),
  1027. async click() {
  1028. if (layer.path) { clipboard.writeText(layer.path); }
  1029. },
  1030. },
  1031. { type: 'separator' },
  1032. {
  1033. label: Editor.I18n.t('ENGINE.menu.copy_property_value'),
  1034. click() {
  1035. const { type = '', value, enumList } = layer;
  1036. const storeData = {
  1037. type,
  1038. value,
  1039. enumList,
  1040. };
  1041. clipboard.writeText(JSON.stringify(storeData));
  1042. },
  1043. },
  1044. {
  1045. label: Editor.I18n.t('ENGINE.menu.paste_property_value'),
  1046. enabled: pasteEnable,
  1047. click() {
  1048. const select = panel.$.nodeLayerSelect.querySelector('ui-select');
  1049. if (select) {
  1050. select.value = store.value;
  1051. layer.value = store.value;
  1052. if (layer.values) {
  1053. layer.values.forEach((val, index) => dump.values[index] = store.value);
  1054. }
  1055. select.dispatch('change');
  1056. select.dispatch('confirm');
  1057. }
  1058. },
  1059. },
  1060. ],
  1061. });
  1062. });
  1063. },
  1064. async update() {
  1065. const panel = this;
  1066. panel.componentCacheExpand = {};
  1067. if (!panel.dump || panel.dump.isScene) {
  1068. return;
  1069. }
  1070. panel.$this.setAttribute('sub-type', 'node');
  1071. panel.$.container.setAttribute('droppable', panel.dropConfig && Object.keys(panel.dropConfig).join());
  1072. panel.$.nodePosition.render(panel.dump.position);
  1073. panel.$.nodeRotation.render(panel.dump.rotation);
  1074. panel.$.nodeScale.render(panel.dump.scale);
  1075. panel.$.nodeMobility.render(panel.dump.mobility);
  1076. setLabel(panel.dump.layer, panel.$.nodeLayer);
  1077. // 查找需要渲染的 component 列表
  1078. const componentList = [];
  1079. for (let i = 0; i < panel.dump.__comps__.length; i++) {
  1080. const comp = panel.dump.__comps__[i];
  1081. if (
  1082. panel.dumps.every((dump) => {
  1083. return dump.__comps__[i] && dump.__comps__[i].type === comp.type;
  1084. })
  1085. ) {
  1086. componentList.push(comp);
  1087. }
  1088. }
  1089. const sectionBody = panel.$.sectionBody;
  1090. const isNotEmpty = componentList.length && sectionBody.__sections__ && sectionBody.__sections__.length;
  1091. const isSameLength = isNotEmpty && sectionBody.__sections__.length === componentList.length;
  1092. const isAllSameType =
  1093. isSameLength &&
  1094. componentList.every((comp, i) => {
  1095. return (
  1096. comp.type === sectionBody.__sections__[i].__type__ &&
  1097. comp.mountedRoot === sectionBody.__sections__[i].dump?.mountedRoot
  1098. );
  1099. });
  1100. // 如果元素长度、类型一致,则直接更新现有的界面
  1101. if (isAllSameType) {
  1102. for (let index = 0; index < sectionBody.__sections__.length; index++) {
  1103. const $section = sectionBody.__sections__[index];
  1104. const dump = componentList[index];
  1105. $section.dump = dump;
  1106. // 处理 ui-checkbox 涉及多选的情况
  1107. const $active = $section.querySelector('ui-checkbox');
  1108. $active.dump = dump.value.enabled;
  1109. $active.value = dump.value.enabled.value;
  1110. if ($active.dump.values && $active.dump.values.some((ds) => ds !== $active.dump.value)) {
  1111. $active.invalid = true;
  1112. } else {
  1113. $active.invalid = false;
  1114. }
  1115. const $link = $section.querySelector('ui-link');
  1116. panel.setHelpUrl($link, dump.editor);
  1117. await Promise.all($section.__panels__.map(($panel) => {
  1118. return $panel.update(dump);
  1119. }));
  1120. }
  1121. } else {
  1122. // 如果元素不一致,说明切换了选中元素,那么需要更新整个界面
  1123. sectionBody.innerText = '';
  1124. sectionBody.__sections__ = [];
  1125. componentList.forEach(async (component, i) => {
  1126. const additional = JSON.stringify([{
  1127. type: component.type,
  1128. value: component.value.uuid.value,
  1129. }]);
  1130. const $section = document.createElement('ui-section');
  1131. $section.setAttribute('expand', '');
  1132. $section.setAttribute('class', 'component config');
  1133. let cacheExpandKey = `node-component:${component.type}`;
  1134. if (panel.componentCacheExpand[cacheExpandKey]) {
  1135. // when exist duplicated component, use uuid as key;
  1136. cacheExpandKey = `node-component:${component.value.uuid.value}`;
  1137. }
  1138. panel.componentCacheExpand[cacheExpandKey] = true;
  1139. $section.setAttribute('cache-expand', `${cacheExpandKey}`);
  1140. $section.innerHTML = `
  1141. <header class="component-header" slot="header">
  1142. <ui-checkbox class="active"></ui-checkbox>
  1143. <ui-drag-item type="${component.type}" types="${component.type}" additional='${additional}'>
  1144. <ui-icon default="component" color="true" value="${component.type}"></ui-icon>
  1145. <span class="name">${component.type}${component.mountedRoot ? '+' : ''}</span>
  1146. </ui-drag-item>
  1147. <ui-link class="link" tooltip="i18n:ENGINE.menu.help_url">
  1148. <ui-icon value="help"></ui-icon>
  1149. </ui-link>
  1150. <ui-icon class="menu" value="menu" tooltip="i18n:ENGINE.menu.component"></ui-icon>
  1151. </header>
  1152. `;
  1153. $section.dump = component;
  1154. $section.__panels__ = [];
  1155. $section.__type__ = component.type;
  1156. const $active = $section.querySelector('ui-checkbox');
  1157. $active.value = component.value.enabled.value;
  1158. $active.dump = component.value.enabled;
  1159. $active.addEventListener('change', (event) => {
  1160. event.stopPropagation();
  1161. const value = !!$active.value;
  1162. const dump = $active.dump;
  1163. dump.value = value;
  1164. if ('values' in dump) {
  1165. dump.values.forEach((val, index) => {
  1166. dump.values[index] = value;
  1167. });
  1168. }
  1169. $active.dispatch('change-dump');
  1170. });
  1171. $active.addEventListener('confirm', (event) => {
  1172. event.stopPropagation();
  1173. $active.dispatch('confirm-dump');
  1174. });
  1175. const $link = $section.querySelector('.link');
  1176. $link.addEventListener('click', (event) => {
  1177. event.stopPropagation();
  1178. });
  1179. panel.setHelpUrl($link, component.editor);
  1180. const $menu = $section.querySelector('.menu');
  1181. $menu.addEventListener('click', (event) => {
  1182. event.stopPropagation();
  1183. exports.methods.componentContextMenu(panel.uuidList, $section.dump, componentList.length, i, panel.dumps);
  1184. });
  1185. sectionBody.__sections__[i] = $section;
  1186. sectionBody.appendChild($section);
  1187. // 排序
  1188. const renderListHeader = panel.renderMap.header[$section.__type__] ?? [];
  1189. let renderListSection = panel.renderMap.section[$section.__type__] ?? [];
  1190. const renderListFooter = panel.renderMap.footer[$section.__type__] ?? [];
  1191. // 如果都没有渲染模板,使用默认 cc.Class 模板
  1192. if (!renderListSection.length) {
  1193. // 判断继承
  1194. if (Array.isArray(component.extends)) {
  1195. const parentClass = component.extends[0];
  1196. renderListSection = panel.renderMap.section[parentClass];
  1197. }
  1198. if (!renderListSection) {
  1199. renderListSection = panel.renderMap.section['cc.Class'];
  1200. }
  1201. }
  1202. let renderList = [...renderListHeader, ...renderListSection, ...renderListFooter];
  1203. renderList.forEach((file) => {
  1204. const $panel = document.createElement('ui-panel');
  1205. $panel.injectionStyle(injectionStyle);
  1206. $panel.setAttribute('src', file);
  1207. $panel.shadowRoot.addEventListener('change-dump', (event) => {
  1208. exports.listeners['change-dump'].call(panel, event);
  1209. });
  1210. $panel.shadowRoot.addEventListener('confirm-dump', (event) => {
  1211. exports.listeners['confirm-dump'].call(panel, event);
  1212. });
  1213. $panel.shadowRoot.addEventListener('reset-dump', (event) => {
  1214. exports.listeners['reset-dump'].call(panel, event);
  1215. });
  1216. $panel.shadowRoot.addEventListener('create-dump', (event) => {
  1217. exports.listeners['create-dump'].call(panel, event);
  1218. });
  1219. $panel.shadowRoot.addEventListener('preview-dump', (event) => {
  1220. exports.listeners['preview-dump'].call(panel, event);
  1221. });
  1222. $section.appendChild($panel);
  1223. $section.__panels__.push($panel);
  1224. $panel.dump = component;
  1225. $panel.messageProtocol = messageProtocol;
  1226. $panel.update(component);
  1227. });
  1228. // 组件丢失的提示
  1229. if (component.type === "cc.MissingScript") {
  1230. const $missTip = document.createElement('div');
  1231. $missTip.style.cssText = "border: 1px solid var(--color-normal-border); padding: 15px; border-radius: 4px;margin-top: 15px;";
  1232. const assetData = await Editor.Message.request('asset-db', 'query-asset-data', component.value.__scriptAsset.value.uuid);
  1233. $missTip.innerHTML = `${assetData ? assetData.url : ''} ${Editor.I18n.t('ENGINE.components.missScriptTip')}`;
  1234. $section.appendChild($missTip);
  1235. }
  1236. });
  1237. }
  1238. // 自定义 node 数据
  1239. if (panel.renderMap.section && panel.renderMap.section['cc.Node']) {
  1240. const array = (panel.$.nodeSection.__node_panels__ = panel.$.nodeSection.__node_panels__ || []);
  1241. panel.renderMap.section['cc.Node'].forEach((file, index) => {
  1242. if (!array[index]) {
  1243. array[index] = document.createElement('ui-panel');
  1244. array[index].injectionStyle(injectionStyle);
  1245. panel.$.nodeSection.appendChild(array[index]);
  1246. }
  1247. array[index].setAttribute('src', file);
  1248. array[index].dump = panel.dump;
  1249. array[index].messageProtocol = messageProtocol;
  1250. array[index].update(panel.dump);
  1251. });
  1252. for (let i = panel.renderMap.section['cc.Node'].length; i < array.length; i++) {
  1253. array[i].remove();
  1254. }
  1255. array.length = panel.renderMap.section['cc.Node'].length;
  1256. } else if (panel.$.nodeSection.__node_panels__) {
  1257. panel.$.nodeSection.__node_panels__.forEach((dom) => {
  1258. dom.remove();
  1259. });
  1260. delete panel.$.nodeSection.__node_panels__;
  1261. }
  1262. },
  1263. close() {
  1264. const panel = this;
  1265. Editor.Message.removeBroadcastListener('i18n:change', panel.i18nChangeBind);
  1266. },
  1267. i18nChange() {
  1268. const panel = this;
  1269. const $links = panel.$.container.querySelectorAll('ui-link');
  1270. $links.forEach($link => panel.setHelpUrl($link));
  1271. },
  1272. getAndParseClipboard() {
  1273. const store = clipboard.readText();
  1274. if (!store) { return; }
  1275. try {
  1276. return JSON.parse(store);
  1277. } catch (err) {
  1278. return;
  1279. }
  1280. },
  1281. validatePasteEnable(dump, store) {
  1282. if (!store) { return false; }
  1283. const { type, value, enumList = [], bitmaskList = [] } = store;
  1284. if (typeof type === 'undefined' || typeof value === 'undefined') { return false; }
  1285. if (type !== dump.type || Boolean(dump.isArray) !== Array.isArray(value) || dump.readonly) { return false; }
  1286. switch (type) {
  1287. case 'BitMask': {
  1288. return bitmaskList.length === dump.bitmaskList?.length && bitmaskList.every((item, index) => {
  1289. return item.name === dump.bitmaskList?.[index].name && item.value === dump.bitmaskList?.[index].value;
  1290. });
  1291. }
  1292. case 'Enum': {
  1293. return enumList.length === dump.enumList?.length && enumList.every((item, index) => {
  1294. return item.name === dump.enumList?.[index].name && item.value === dump.enumList?.[index].value;
  1295. }) && enumList.some(item => item.value === value);
  1296. }
  1297. default: return true;
  1298. }
  1299. },
  1300. },
  1301. missingComponent: {
  1302. ready() {
  1303. const panel = this;
  1304. const sectionMissing = panel.$.sectionMissing;
  1305. sectionMissing.addEventListener('click', (event) => {
  1306. if (event.target.tagName !== 'UI-ICON') {
  1307. return;
  1308. }
  1309. if (!Array.isArray(panel.dump.removedComponents)) {
  1310. return;
  1311. }
  1312. const i = event.target.getAttribute('index');
  1313. const type = event.target.getAttribute('value');
  1314. const info = panel.dump.removedComponents[i];
  1315. if (!info) {
  1316. return;
  1317. }
  1318. const uuidList = panel.uuidList;
  1319. switch (type) {
  1320. case 'save-o': {
  1321. Editor.Message.request(messageProtocol.scene, 'apply-removed-component', uuidList[0], info.fileID);
  1322. break;
  1323. }
  1324. case 'reset': {
  1325. Editor.Message.request(messageProtocol.scene, 'revert-removed-component', uuidList[0], info.fileID);
  1326. break;
  1327. }
  1328. }
  1329. });
  1330. },
  1331. update() {
  1332. const panel = this;
  1333. if (!panel.dump || panel.dump.isScene) {
  1334. return;
  1335. }
  1336. const uuidList = panel.uuidList;
  1337. const sectionMissing = panel.$.sectionMissing;
  1338. sectionMissing.__sections__ = sectionMissing.__sections__ || [];
  1339. if (!panel.dump.removedComponents || uuidList.length !== 1) {
  1340. panel.dump.removedComponents = [];
  1341. }
  1342. for (let i = 0; i < panel.dump.removedComponents.length; i++) {
  1343. let $section = sectionMissing.__sections__[i];
  1344. if (!$section) {
  1345. $section = document.createElement('section');
  1346. sectionMissing.__sections__[i] = $section;
  1347. sectionMissing.appendChild($section);
  1348. }
  1349. $section.innerHTML = `
  1350. <span class="name"><span>${panel.dump.removedComponents[i].name}</span> [removed]</span>
  1351. <ui-icon value="reset" index="${i}" tooltip="i18n:ENGINE.prefab.reset"></ui-icon>
  1352. <ui-icon value="save-o" index="${i}" tooltip="i18n:ENGINE.prefab.save"></ui-icon>
  1353. `;
  1354. }
  1355. while (sectionMissing.__sections__.length > panel.dump.removedComponents.length) {
  1356. const $section = sectionMissing.__sections__.pop();
  1357. $section.parentElement.removeChild($section);
  1358. }
  1359. },
  1360. },
  1361. layer: {
  1362. ready() {
  1363. const panel = this;
  1364. panel.$.nodeLayerButton.addEventListener('change', (event) => {
  1365. event.stopPropagation();
  1366. Editor.Message.send('project', 'open-settings', 'project', 'layer');
  1367. });
  1368. },
  1369. update() {
  1370. const panel = this;
  1371. if (!panel.dump || panel.dump.isScene) {
  1372. return;
  1373. }
  1374. panel.$.nodeLayerSelect.render(panel.dump.layer);
  1375. let prevValues = [panel.dump.layer.value];
  1376. if (panel.dump.layer.values) {
  1377. prevValues = panel.dump.layer.values.slice();
  1378. }
  1379. panel.$.nodeLayerSelect.prevValues = prevValues;
  1380. },
  1381. },
  1382. footer: {
  1383. ready() {
  1384. const panel = this;
  1385. panel.$.componentAdd.addEventListener('click', () => {
  1386. Editor.Panel.__protected__.openKit('ui-kit.searcher', {
  1387. elem: panel.$.componentAdd,
  1388. params: [
  1389. {
  1390. type: 'add-component',
  1391. },
  1392. ],
  1393. listeners: {
  1394. async confirm(detail/* info */) {
  1395. if (!detail) { return; }
  1396. // 批量调用request意味着编辑操作在很多帧后才会完成,所以不能自动记录undo
  1397. const undoID = await beginRecording(panel.uuidList);
  1398. for (const uuid of panel.uuidList) {
  1399. await Editor.Message.request(messageProtocol.scene, 'create-component', {
  1400. uuid,
  1401. component: detail.info.cid,
  1402. });
  1403. }
  1404. if (detail.info.name) {
  1405. trackEventWithTimer('laber', `A100000_${detail.info.name}`);
  1406. }
  1407. await endRecording(undoID);
  1408. },
  1409. },
  1410. });
  1411. });
  1412. },
  1413. update() {},
  1414. },
  1415. materials: {
  1416. async update() {
  1417. const panel = this;
  1418. const materialPanels = [];
  1419. const materialPanelType = 'asset';
  1420. const oldChildren = Array.from(panel.$.sectionAsset.children);
  1421. const materialUuids = panel.assets['cc.Material'];
  1422. let materialPrevPanel = null;
  1423. for (const materialUuid in materialUuids) {
  1424. let materialPanel = oldChildren.find((child) => child.getAttribute('uuid') === materialUuid);
  1425. if (!materialPanel) {
  1426. // 添加新的
  1427. materialPanel = document.createElement('ui-panel');
  1428. materialPanel.injectionStyle(injectionStyle);
  1429. materialPanel.setAttribute('src', panel.typeManager[materialPanelType]);
  1430. materialPanel.setAttribute('type', materialPanelType);
  1431. materialPanel.setAttribute('sub-type', 'unknown');
  1432. materialPanel.setAttribute('uuid', materialUuid);
  1433. materialPanel.panelObject.replaceContainerWithUISection({
  1434. type: materialPanelType,
  1435. uuid: materialUuid,
  1436. });
  1437. const { section = {} } = panel.renderManager[materialPanelType];
  1438. // 按数组顺序放置
  1439. if (materialPrevPanel) {
  1440. materialPrevPanel.after(materialPanel);
  1441. } else {
  1442. panel.$.sectionAsset.prepend(materialPanel);
  1443. }
  1444. // call update after panel is connected(ensure lifecycle hook `ready` has been called)
  1445. materialPanel.update([materialUuid], { section });
  1446. materialPanel.focusEventInNode = () => {
  1447. const children = Array.from(materialPanel.parentElement.children);
  1448. children.forEach((child) => {
  1449. if (child === materialPanel) {
  1450. child.setAttribute('focused', '');
  1451. } else {
  1452. child.removeAttribute('focused');
  1453. }
  1454. });
  1455. };
  1456. materialPanel.blurEventInNode = () => {
  1457. if (panel.blurSleep) {
  1458. return;
  1459. }
  1460. materialPanel.removeAttribute('focused');
  1461. };
  1462. materialPanel.addEventListener('focus', materialPanel.focusEventInNode);
  1463. materialPanel.addEventListener('blur', materialPanel.blurEventInNode);
  1464. }
  1465. materialPanels.push(materialPanel);
  1466. materialPrevPanel = materialPanel;
  1467. }
  1468. // 删除多余的
  1469. for (const oldChild of oldChildren) {
  1470. if (oldChild && materialPanels.indexOf(oldChild) === -1) {
  1471. await oldChild.panel.beforeClose.call(oldChild.panelObject);
  1472. oldChild.removeEventListener('focus', oldChild.focusEventInNode);
  1473. oldChild.removeEventListener('blur', oldChild.blurEventInNode);
  1474. oldChild.focusEventInNode = undefined;
  1475. oldChild.blurEventInNode = undefined;
  1476. oldChild.remove();
  1477. }
  1478. }
  1479. },
  1480. async beforeClose() {
  1481. const panel = this;
  1482. const children = Array.from(panel.$.sectionAsset.children);
  1483. for (const materialPanel of children) {
  1484. const next = await materialPanel.panel.beforeClose.call(materialPanel.panelObject);
  1485. if (next === false) {
  1486. return false;
  1487. } else {
  1488. materialPanel.removeEventListener('focus', materialPanel.focusEventInNode);
  1489. materialPanel.removeEventListener('blur', materialPanel.blurEventInNode);
  1490. materialPanel.focusEventInNode = undefined;
  1491. materialPanel.blurEventInNode = undefined;
  1492. materialPanel.remove();
  1493. }
  1494. }
  1495. return true;
  1496. },
  1497. },
  1498. };
  1499. exports.methods = {
  1500. undo() {
  1501. this.restore('undo');
  1502. },
  1503. redo() {
  1504. this.restore('redo');
  1505. },
  1506. restore(cmd) {
  1507. if (!cmd) {
  1508. return;
  1509. }
  1510. const panel = this;
  1511. panel.blurSleep = true;
  1512. clearTimeout(panel.blurSleepTimeId);
  1513. panel.blurSleepTimeId = setTimeout(() => {
  1514. panel.blurSleep = false;
  1515. }, 1000);
  1516. const children = Array.from(panel.$.sectionAsset.children);
  1517. for (const materialPanel of children) {
  1518. if (materialPanel.hasAttribute('focused')) {
  1519. materialPanel.panelObject[cmd]();
  1520. return;
  1521. }
  1522. }
  1523. Editor.Message.send(messageProtocol.scene, cmd);
  1524. },
  1525. setHelpUrl($link, data) {
  1526. if (data) {
  1527. $link.helpData = data;
  1528. } else {
  1529. if (!$link.helpData) {
  1530. return;
  1531. }
  1532. data = $link.helpData;
  1533. }
  1534. const url = this.getHelpUrl(data);
  1535. if (url) {
  1536. $link.setAttribute('value', url);
  1537. } else {
  1538. $link.removeAttribute('value');
  1539. }
  1540. },
  1541. /**
  1542. * 获取组件帮助菜单的 url
  1543. * @param editor
  1544. */
  1545. getHelpUrl(data) {
  1546. if (!data || !data.help) {
  1547. return '';
  1548. }
  1549. const help = data.help;
  1550. /**
  1551. * 约定的规则
  1552. * 翻译的都需要 i18n: 开头
  1553. * 没有的话属于直接是配置值的方式,配什么返回什么
  1554. */
  1555. if (!help.startsWith('i18n:')) {
  1556. return help;
  1557. }
  1558. const i18nKey = help.substr(5);
  1559. const url = Editor.I18n.t('ENGINE.help.' + i18nKey);
  1560. if (url) {
  1561. return url;
  1562. }
  1563. /**
  1564. * 再在编辑器内部查找翻译一次
  1565. * 结果可能为空,也是一种需求,即组件配置了但没有合适的文档配置
  1566. */
  1567. return Editor.I18n.t(i18nKey);
  1568. },
  1569. /**
  1570. * 组件上的右键菜单
  1571. */
  1572. componentContextMenu(uuidList, dump, total, index, nodeDumps) {
  1573. // 是否多选节点
  1574. const isMultiple = uuidList.length > 1 ? true : false;
  1575. const uuid = uuidList[0];
  1576. const clipboardComponentInfo = Editor.Clipboard.read('_dump_component_');
  1577. Editor.Menu.popup({
  1578. menu: [
  1579. {
  1580. label: Editor.I18n.t('ENGINE.menu.reset_component'),
  1581. async click() {
  1582. const values = dump.value.uuid.values || [dump.value.uuid.value];
  1583. const undoID = await beginRecording(values);
  1584. for (const compUuid of values) {
  1585. await Editor.Message.request(messageProtocol.scene, 'reset-component', {
  1586. uuid: compUuid,
  1587. });
  1588. }
  1589. await endRecording(undoID);
  1590. },
  1591. },
  1592. { type: 'separator' },
  1593. {
  1594. label: Editor.I18n.t('ENGINE.menu.remove_component'),
  1595. async click() {
  1596. const values = dump.value.uuid.values || [dump.value.uuid.value];
  1597. // 收集待修改的uuids
  1598. const uuids = [];
  1599. const indexes = [];
  1600. for (const value of values) {
  1601. for (const nodeDump of nodeDumps) {
  1602. const uuid = nodeDump.uuid.value;
  1603. const index = nodeDump.__comps__.findIndex((dumpData) => dumpData.value.uuid.value === value);
  1604. if (index !== -1) {
  1605. uuids.push(uuid);
  1606. indexes.push(index);
  1607. if (nodeDump.__comps__[index].type) {
  1608. trackEventWithTimer('laber', `A100001_${nodeDump.__comps__[index].type}`);
  1609. }
  1610. }
  1611. }
  1612. }
  1613. if (!uuids.length > 0) { return; }
  1614. const undoID = await beginRecording(uuids);
  1615. for (let i = 0; i < uuids.length; i++) {
  1616. await Editor.Message.request(messageProtocol.scene, 'remove-array-element', {
  1617. uuid: uuids[i],
  1618. path: '__comps__',
  1619. index: indexes[i],
  1620. });
  1621. }
  1622. await endRecording(undoID);
  1623. },
  1624. },
  1625. {
  1626. label: Editor.I18n.t('ENGINE.menu.move_up_component'),
  1627. enabled: !isMultiple && index !== 0,
  1628. async click() {
  1629. const undoID = await beginRecording(uuid);
  1630. await Editor.Message.request(messageProtocol.scene, 'move-array-element', {
  1631. uuid,
  1632. path: '__comps__',
  1633. target: index,
  1634. offset: -1,
  1635. });
  1636. await endRecording(undoID);
  1637. },
  1638. },
  1639. {
  1640. label: Editor.I18n.t('ENGINE.menu.move_down_component'),
  1641. enabled: !isMultiple && index !== total - 1,
  1642. async click() {
  1643. const undoID = await beginRecording(uuid);
  1644. await Editor.Message.request(messageProtocol.scene, 'move-array-element', {
  1645. uuid,
  1646. path: '__comps__',
  1647. target: index,
  1648. offset: 1,
  1649. });
  1650. await endRecording(undoID);
  1651. },
  1652. },
  1653. { type: 'separator' },
  1654. {
  1655. label: Editor.I18n.t('ENGINE.menu.copy_component'),
  1656. enabled: !isMultiple,
  1657. click() {
  1658. const info = JSON.parse(JSON.stringify(dump));
  1659. delete info.value.__prefab;
  1660. Editor.Clipboard.write('_dump_component_', {
  1661. cid: dump.cid,
  1662. dump: info,
  1663. });
  1664. },
  1665. },
  1666. {
  1667. label: Editor.I18n.t('ENGINE.menu.paste_component_values'),
  1668. enabled: !!(clipboardComponentInfo && clipboardComponentInfo.cid === dump.cid),
  1669. async click() {
  1670. const values = dump.value.uuid.values || [dump.value.uuid.value];
  1671. const uuids = [];
  1672. const indexes = [];
  1673. for (const value of values) {
  1674. for (const nodeDump of nodeDumps) {
  1675. const uuid = nodeDump.uuid.value;
  1676. const index = nodeDump.__comps__.findIndex((dumpData) => dumpData.value.uuid.value === value);
  1677. if (index !== -1) {
  1678. uuids.push(uuid);
  1679. indexes.push(index);
  1680. }
  1681. }
  1682. }
  1683. const undoID = await beginRecording(uuids);
  1684. // 遍历uuids
  1685. for (let i = 0; i < uuids.length; i++) {
  1686. const uuid = uuids[i];
  1687. const index = indexes[i];
  1688. const nodeDump = nodeDumps.find(nodeDump => uuid === nodeDump.uuid.value);
  1689. await Editor.Message.request(messageProtocol.scene, 'set-property', {
  1690. uuid,
  1691. path: nodeDump.__comps__[index].path,
  1692. dump: clipboardComponentInfo.dump,
  1693. });
  1694. }
  1695. await endRecording(undoID);
  1696. },
  1697. },
  1698. { type: 'separator' },
  1699. {
  1700. // 这个按钮不该出现在 component 上,应该在节点上
  1701. label: Editor.I18n.t('ENGINE.menu.paste_component'),
  1702. enabled: !!clipboardComponentInfo,
  1703. async click() {
  1704. const undoID = await beginRecording(uuidList);
  1705. const values = dump.value.uuid.values || [dump.value.uuid.value];
  1706. let index = 0;
  1707. for (const dump of values) {
  1708. const uuid = uuidList[index];
  1709. await Editor.Message.request(messageProtocol.scene, 'create-component', {
  1710. uuid,
  1711. component: clipboardComponentInfo.cid,
  1712. });
  1713. // 检查是否创建成功,是的话,给赋值
  1714. const nodeDump = await Editor.Message.request(messageProtocol.scene, 'query-node', uuid);
  1715. const length = nodeDump.__comps__ && nodeDump.__comps__.length;
  1716. if (length) {
  1717. const lastIndex = length - 1;
  1718. const lastComp = nodeDump.__comps__[lastIndex];
  1719. if (lastComp?.cid === clipboardComponentInfo.cid) {
  1720. await Editor.Message.request(messageProtocol.scene, 'set-property', {
  1721. uuid,
  1722. path: `__comps__.${lastIndex}`,
  1723. dump: clipboardComponentInfo.dump,
  1724. });
  1725. }
  1726. }
  1727. index++;
  1728. }
  1729. await endRecording(undoID);
  1730. },
  1731. },
  1732. ],
  1733. });
  1734. },
  1735. nodeContextMenu(uuidList, dumps) {
  1736. const dump = dumps[0];
  1737. // 是否多选节点
  1738. const isMultiple = dump.length > 1 ? true : false;
  1739. const clipboardNodeInfo = Editor.Clipboard.read('_dump_node_');
  1740. const clipboardNodeWorldTransform = Editor.Clipboard.read('_dump_node_world_transform_');
  1741. const clipboardComponentInfo = Editor.Clipboard.read('_dump_component_');
  1742. function notEqualDefaultValueVec3(propName) {
  1743. const keys = ['x', 'y', 'z'];
  1744. return keys.some(key => {
  1745. return dump[propName].value[key] !== dump[propName].default.value[key].value;
  1746. });
  1747. }
  1748. Editor.Menu.popup({
  1749. menu: [
  1750. {
  1751. label: Editor.I18n.t('ENGINE.menu.reset_node'),
  1752. enabled: !dump.position.readonly && !dump.rotation.readonly && !dump.scale.readonly,
  1753. async click() {
  1754. const undoID = await beginRecording(uuidList);
  1755. for (const uuid of uuidList) {
  1756. await Editor.Message.request(messageProtocol.scene, 'reset-node', {
  1757. uuid,
  1758. });
  1759. }
  1760. await endRecording(undoID);
  1761. },
  1762. },
  1763. { type: 'separator' },
  1764. {
  1765. label: Editor.I18n.t('ENGINE.menu.copy_node_value'),
  1766. enabled: !isMultiple,
  1767. async click() {
  1768. Editor.Clipboard.write('_dump_node_', {
  1769. type: dump.type,
  1770. attrs: ['position', 'rotation', 'scale', 'mobility', 'layer'],
  1771. dump: JSON.parse(JSON.stringify(dump)),
  1772. });
  1773. },
  1774. },
  1775. {
  1776. label: Editor.I18n.t('ENGINE.menu.paste_node_value'),
  1777. enabled: !!clipboardNodeInfo,
  1778. async click() {
  1779. const undoID = await beginRecording(uuidList);
  1780. for (const uuid of uuidList) {
  1781. for (const attr of clipboardNodeInfo.attrs) {
  1782. await Editor.Message.request(messageProtocol.scene, 'set-property', {
  1783. uuid,
  1784. path: attr,
  1785. dump: clipboardNodeInfo.dump[attr],
  1786. });
  1787. }
  1788. }
  1789. await endRecording(undoID);
  1790. },
  1791. },
  1792. { type: 'separator' },
  1793. {
  1794. label: Editor.I18n.t('ENGINE.menu.copy_node_world_transform'),
  1795. enabled: !isMultiple,
  1796. async click() {
  1797. const data = await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
  1798. name: 'inspector',
  1799. method: 'queryNodeWorldTransform',
  1800. args: [dump.uuid.value],
  1801. });
  1802. if (data) {
  1803. Editor.Clipboard.write('_dump_node_world_transform_', {
  1804. data,
  1805. });
  1806. }
  1807. },
  1808. },
  1809. {
  1810. label: Editor.I18n.t('ENGINE.menu.paste_node_world_transform'),
  1811. enabled: !!clipboardNodeWorldTransform,
  1812. async click() {
  1813. if (clipboardNodeWorldTransform.data) {
  1814. const undoID = await beginRecording(uuidList);
  1815. for (const uuid of uuidList) {
  1816. await Editor.Message.request(messageProtocol.scene, 'execute-scene-script', {
  1817. name: 'inspector',
  1818. method: 'setNodeWorldTransform',
  1819. args: [uuid, clipboardNodeWorldTransform.data],
  1820. });
  1821. }
  1822. await endRecording(undoID);
  1823. }
  1824. },
  1825. },
  1826. { type: 'separator' },
  1827. {
  1828. label: Editor.I18n.t('ENGINE.menu.paste_component'),
  1829. enabled: !!clipboardComponentInfo,
  1830. async click() {
  1831. const undoID = await beginRecording(uuidList);
  1832. for (const uuid of uuidList) {
  1833. await Editor.Message.request(messageProtocol.scene, 'create-component', {
  1834. uuid,
  1835. component: clipboardComponentInfo.cid,
  1836. });
  1837. // 检查是否创建成功,是的话,给赋值
  1838. const nodeDump = await Editor.Message.request(messageProtocol.scene, 'query-node', uuid);
  1839. const length = nodeDump.__comps__ && nodeDump.__comps__.length;
  1840. if (length) {
  1841. const lastIndex = length - 1;
  1842. const lastComp = nodeDump.__comps__[lastIndex];
  1843. if (lastComp?.cid === clipboardComponentInfo.cid) {
  1844. await Editor.Message.request(messageProtocol.scene, 'set-property', {
  1845. uuid,
  1846. path: `__comps__.${lastIndex}`,
  1847. dump: clipboardComponentInfo.dump,
  1848. });
  1849. }
  1850. }
  1851. }
  1852. await endRecording(undoID);
  1853. },
  1854. },
  1855. { type: 'separator' },
  1856. {
  1857. label: Editor.I18n.t('ENGINE.menu.reset_node_position'),
  1858. enabled: !dump.position.readonly && notEqualDefaultValueVec3('position'),
  1859. async click() {
  1860. const undoID = await beginRecording(uuidList);
  1861. for (const uuid of uuidList) {
  1862. await Editor.Message.request(messageProtocol.scene, 'reset-property', {
  1863. uuid,
  1864. path: 'position',
  1865. });
  1866. }
  1867. await endRecording(undoID);
  1868. },
  1869. },
  1870. {
  1871. label: Editor.I18n.t('ENGINE.menu.reset_node_rotation'),
  1872. enabled: !dump.rotation.readonly && notEqualDefaultValueVec3('rotation'),
  1873. async click() {
  1874. const undoID = await beginRecording(uuidList);
  1875. for (const uuid of uuidList) {
  1876. await Editor.Message.request(messageProtocol.scene, 'reset-property', {
  1877. uuid,
  1878. path: 'rotation',
  1879. });
  1880. }
  1881. await endRecording(undoID);
  1882. },
  1883. },
  1884. {
  1885. label: Editor.I18n.t('ENGINE.menu.reset_node_scale'),
  1886. enabled: !dump.scale.readonly && notEqualDefaultValueVec3('scale'),
  1887. async click() {
  1888. const undoID = await beginRecording(uuidList);
  1889. for (const uuid of uuidList) {
  1890. await Editor.Message.request(messageProtocol.scene, 'reset-property', {
  1891. uuid,
  1892. path: 'scale',
  1893. });
  1894. }
  1895. await endRecording(undoID);
  1896. },
  1897. },
  1898. {
  1899. label: Editor.I18n.t('ENGINE.menu.reset_node_mobility'),
  1900. enabled: !dump.mobility.readonly && dump.mobility.value !== dump.mobility.default,
  1901. async click() {
  1902. const undoID = await beginRecording(uuidList);
  1903. for (const uuid of uuidList) {
  1904. await Editor.Message.request(messageProtocol.scene, 'reset-property', {
  1905. uuid,
  1906. path: 'mobility',
  1907. });
  1908. }
  1909. await endRecording(undoID);
  1910. },
  1911. },
  1912. ],
  1913. });
  1914. },
  1915. async replaceAssetUuidInNodes(assetUuid, newAssetUuid) {
  1916. const panel = this;
  1917. const materialUuids = panel.assets['cc.Material'];
  1918. if (!materialUuids) {
  1919. return;
  1920. }
  1921. try {
  1922. const undoID = await beginRecording(panel.uuidList);
  1923. for (const dumpPath in materialUuids[assetUuid]) {
  1924. const dumpData = materialUuids[assetUuid][dumpPath];
  1925. for (let i = 0; i < panel.uuidList.length; i++) {
  1926. const nodeUuid = panel.uuidList[i];
  1927. await Editor.Message.request(messageProtocol.scene, 'set-property', {
  1928. uuid: nodeUuid,
  1929. path: dumpPath,
  1930. dump: {
  1931. type: dumpData.type,
  1932. value: { uuid: newAssetUuid },
  1933. },
  1934. });
  1935. }
  1936. }
  1937. await endRecording(undoID);
  1938. } catch (error) {
  1939. console.error(error);
  1940. }
  1941. },
  1942. toggleShowAddComponentBtn(show) {
  1943. this.$.componentAdd.style.display = show ? 'inline-flex' : 'none';
  1944. },
  1945. isAnimationMode() {
  1946. return Editor.EditMode.getMode() === 'animation';
  1947. },
  1948. handlerSceneChangeMode() {
  1949. this.toggleShowAddComponentBtn(!this.isAnimationMode()); // 动画编辑模式下,要隐藏按钮
  1950. },
  1951. };
  1952. exports.update = async function update(uuidList, renderMap, dropConfig, typeManager, renderManager) {
  1953. const panel = this;
  1954. const enginePath = path.join('editor', 'inspector', 'components');
  1955. Object.values(renderMap).forEach((config) => {
  1956. Object.values(config).forEach((renders) => {
  1957. renders.sort((a, b) => {
  1958. return b.indexOf(enginePath) - a.indexOf(enginePath);
  1959. });
  1960. });
  1961. });
  1962. panel.uuidList = uuidList || [];
  1963. panel.renderMap = renderMap;
  1964. panel.dropConfig = dropConfig;
  1965. panel.typeManager = typeManager;
  1966. panel.renderManager = renderManager;
  1967. for (const prop in Elements) {
  1968. const element = Elements[prop];
  1969. if (element.update) {
  1970. await element.update.call(panel);
  1971. }
  1972. }
  1973. };
  1974. exports.ready = async function ready() {
  1975. const panel = this;
  1976. // 为了避免把 ui-num-input, ui-color 的连续 change 进行 snapshot
  1977. panel.snapshotLock = false;
  1978. // 节点的 ipc 协议,指向 scene 或 xr-scene 等进程
  1979. panel.__queryMessageProtocolScene__ = async function() {
  1980. try {
  1981. if (!panel.messageProtocol) {
  1982. panel.messageProtocol = messageProtocol;
  1983. }
  1984. const config = await await Editor.Profile.getConfig('inspector', 'message-protocol');
  1985. if (config) {
  1986. Object.assign(messageProtocol, config);
  1987. }
  1988. } catch (error) {
  1989. console.error(error);
  1990. messageProtocol.scene = 'scene';
  1991. }
  1992. };
  1993. await panel.__queryMessageProtocolScene__();
  1994. for (const prop in Elements) {
  1995. const element = Elements[prop];
  1996. if (element.ready) {
  1997. element.ready.call(panel);
  1998. }
  1999. }
  2000. this.replaceAssetUuidInNodesBind = this.replaceAssetUuidInNodes.bind(this);
  2001. this.handlerSceneChangeModeBind = this.handlerSceneChangeMode.bind(this);
  2002. Editor.Message.addBroadcastListener('inspector:replace-asset-uuid-in-nodes', this.replaceAssetUuidInNodesBind);
  2003. Editor.Message.addBroadcastListener('scene:change-mode', this.handlerSceneChangeModeBind);
  2004. };
  2005. exports.close = async function close() {
  2006. const panel = this;
  2007. for (const prop in Elements) {
  2008. const element = Elements[prop];
  2009. if (element.close) {
  2010. element.close.call(panel);
  2011. }
  2012. }
  2013. Editor.Message.removeBroadcastListener('inspector:replace-asset-uuid-in-nodes', this.replaceAssetUuidInNodesBind);
  2014. Editor.Message.removeBroadcastListener('scene:change-mode', this.handlerSceneChangeModeBind);
  2015. };
  2016. exports.beforeClose = async function beforeClose() {
  2017. const panel = this;
  2018. for (const prop in Elements) {
  2019. const element = Elements[prop];
  2020. if (element.beforeClose) {
  2021. const next = await element.beforeClose.call(panel);
  2022. if (next === false) {
  2023. return false;
  2024. }
  2025. }
  2026. }
  2027. return true;
  2028. };
  2029. exports.config = {
  2030. section: require('../components.js'),
  2031. footer: require('../components-footer.js'),
  2032. };