particle-system.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. /* eslint-disable @typescript-eslint/no-unsafe-return */
  2. const propUtils = require('../utils/prop');
  3. exports.template = /* html*/`
  4. <div class="particle-system-component">
  5. <div class="content">
  6. <ui-prop type="dump" key="duration"></ui-prop>
  7. <ui-prop type="dump" key="capacity"></ui-prop>
  8. <ui-prop type="dump" key="loop"></ui-prop>
  9. <ui-prop type="dump" key="playOnAwake"></ui-prop>
  10. <ui-prop type="dump" key="prewarm"></ui-prop>
  11. <ui-prop type="dump" key="simulationSpace"></ui-prop>
  12. <ui-prop type="dump" key="simulationSpeed"></ui-prop>
  13. <ui-prop type="dump" key="startDelay"></ui-prop>
  14. <ui-prop type="dump" key="startLifetime"></ui-prop>
  15. <ui-prop type="dump" key="startColor"></ui-prop>
  16. <ui-prop type="dump" key="scaleSpace"></ui-prop>
  17. <ui-prop type="dump" key="startSize3D"></ui-prop>
  18. <!-- hack changeName if startSize3D change -->
  19. <ui-prop type="dump" key="startSizeX" displayName="Start Size" showflag="!startSize3D"></ui-prop>
  20. <ui-prop type="dump" class="indent" key="startSizeX" showflag="startSize3D"></ui-prop>
  21. <ui-prop type="dump" class="indent" key="startSizeY"></ui-prop>
  22. <ui-prop type="dump" class="indent" key="startSizeZ"></ui-prop>
  23. <ui-prop type="dump" key="startSpeed"></ui-prop>
  24. <ui-prop type="dump" key="startRotation3D"></ui-prop>
  25. <ui-prop type="dump" class="indent" key="startRotationX"></ui-prop>
  26. <ui-prop type="dump" class="indent" key="startRotationY"></ui-prop>
  27. <!-- hack changeName if startRotation3D change -->
  28. <ui-prop type="dump" class="indent" key="startRotationZ" showflag="startRotation3D"></ui-prop>
  29. <ui-prop type="dump" showflag="!startRotation3D" displayName="Start Rotation" key="startRotationZ"></ui-prop>
  30. <ui-prop type="dump" key="gravityModifier"></ui-prop>
  31. <ui-prop type="dump" key="rateOverTime"></ui-prop>
  32. <ui-prop type="dump" key="rateOverDistance"></ui-prop>
  33. <ui-prop type="dump" key="bursts"></ui-prop>
  34. <!-- Render other data that has not taken over -->
  35. <div id="customProps"></div>
  36. <ui-section key="renderCulling" autoExpand cache-expand="particle-system-cullingMode">
  37. <ui-prop slot="header" no-label class="header" empty="true" labelflag="renderCulling" key="renderCulling">
  38. <ui-checkbox></ui-checkbox><ui-label></ui-label>
  39. </ui-prop>
  40. <ui-prop type="dump" key="cullingMode" disableflag="!renderCulling"></ui-prop>
  41. <ui-prop type="dump" key="aabbHalfX" disableflag="!renderCulling"></ui-prop>
  42. <ui-prop type="dump" key="aabbHalfY" disableflag="!renderCulling"></ui-prop>
  43. <ui-prop type="dump" key="aabbHalfZ" disableflag="!renderCulling"></ui-prop>
  44. <ui-prop empty="true" disableflag="!renderCulling">
  45. <ui-label slot="label" value="Show Bounds"></ui-label>
  46. <ui-checkbox slot="content" id="showBounds"></ui-checkbox>
  47. </ui-prop>
  48. <ui-button id="resetBounds" style="width:200px;margin: 4px auto 0 auto;">Regenerate bounding box</ui-button>
  49. </ui-section>
  50. <ui-section key="noiseModule.value.enable" autoExpand cache-expand="particle-system-useNoise">
  51. <ui-prop slot="header" no-label class="header" empty="true" key="noiseModule.value.enable">
  52. <ui-checkbox></ui-checkbox><ui-label value="Noise Module"></ui-label>
  53. </ui-prop>
  54. <ui-prop>
  55. <ui-label slot="label" value="Noise Preview"></ui-label>
  56. <div slot="content" style="display: flex;flex-direction: row-reverse;padding: 5px;">
  57. <canvas id="noisePreview" width="100" height="100"></canvas>
  58. </div>
  59. </ui-prop>
  60. <ui-prop type="dump" key="noiseModule.value.strengthX" disableflag="!noiseModule.value.enable"></ui-prop>
  61. <ui-prop type="dump" key="noiseModule.value.strengthY" disableflag="!noiseModule.value.enable"></ui-prop>
  62. <ui-prop type="dump" key="noiseModule.value.strengthZ" disableflag="!noiseModule.value.enable"></ui-prop>
  63. <ui-prop type="dump" key="noiseModule.value.noiseSpeedX" disableflag="!noiseModule.value.enable"></ui-prop>
  64. <ui-prop type="dump" key="noiseModule.value.noiseSpeedY" disableflag="!noiseModule.value.enable"></ui-prop>
  65. <ui-prop type="dump" key="noiseModule.value.noiseSpeedZ" disableflag="!noiseModule.value.enable"></ui-prop>
  66. <ui-prop type="dump" key="noiseModule.value.noiseFrequency" disableflag="!noiseModule.value.enable"></ui-prop>
  67. <ui-prop type="dump" key="noiseModule.value.remapX" disableflag="!noiseModule.value.enable"></ui-prop>
  68. <ui-prop type="dump" key="noiseModule.value.remapY" disableflag="!noiseModule.value.enable"></ui-prop>
  69. <ui-prop type="dump" key="noiseModule.value.remapZ" disableflag="!noiseModule.value.enable"></ui-prop>
  70. <ui-prop type="dump" key="noiseModule.value.octaves" disableflag="!noiseModule.value.enable"></ui-prop>
  71. <ui-prop type="dump" key="noiseModule.value.octaveMultiplier" disableflag="!noiseModule.value.enable"></ui-prop>
  72. <ui-prop type="dump" key="noiseModule.value.octaveScale" disableflag="!noiseModule.value.enable"></ui-prop>
  73. </ui-section>
  74. <ui-section key="shapeModule" cache-expand="particle-system-shapeModule">
  75. <ui-prop slot="header" no-label class="header" type="dump" key="shapeModule.value.enable" labelflag="shapeModule"
  76. empty="true">
  77. <ui-checkbox></ui-checkbox><ui-label></ui-label>
  78. </ui-prop>
  79. <ui-prop type="dump" key="shapeModule.value.shapeType"></ui-prop>
  80. <ui-prop empty="true" labelflag="shapeModule.value.emitFrom" type="dump" key="shapeModule.value.emitFrom">
  81. <ui-label slot="label"></ui-label>
  82. <ui-select slot="content" id="emitFromSelect"></ui-select>
  83. </ui-prop>
  84. <ui-prop type="dump" key="shapeModule.value.radius"></ui-prop>
  85. <ui-prop type="dump" key="shapeModule.value.radiusThickness"></ui-prop>
  86. <ui-prop type="dump" key="shapeModule.value.angle"></ui-prop>
  87. <ui-prop type="dump" key="shapeModule.value.arc"></ui-prop>
  88. <ui-prop type="dump" key="shapeModule.value.arcMode"></ui-prop>
  89. <ui-prop type="dump" key="shapeModule.value.arcSpread"></ui-prop>
  90. <ui-prop type="dump" key="shapeModule.value.arcSpeed"></ui-prop>
  91. <ui-prop type="dump" key="shapeModule.value.length"></ui-prop>
  92. <ui-prop type="dump" key="shapeModule.value.boxThickness"></ui-prop>
  93. <ui-prop type="dump" key="shapeModule.value.position"></ui-prop>
  94. <ui-prop type="dump" key="shapeModule.value.rotation"></ui-prop>
  95. <ui-prop type="dump" key="shapeModule.value.scale"></ui-prop>
  96. <ui-prop type="dump" key="shapeModule.value.alignToDirection"></ui-prop>
  97. <ui-prop type="dump" key="shapeModule.value.randomDirectionAmount"></ui-prop>
  98. <ui-prop type="dump" key="shapeModule.value.sphericalDirectionAmount"></ui-prop>
  99. <ui-prop type="dump" key="shapeModule.value.randomPositionAmount"></ui-prop>
  100. </ui-section>
  101. <ui-section key="velocityOvertimeModule" autoflag="true" cache-expand="particle-system-velocityOvertimeModule"></ui-section>
  102. <ui-section key="forceOvertimeModule" autoflag="true" cache-expand="particle-system-forceOvertimeModule"></ui-section>
  103. <ui-section empty="true" key="sizeOvertimeModule"
  104. cache-expand="particle-system-sizeOvertimeModule">
  105. <ui-prop slot="header" no-label class="header" type="dump" key="sizeOvertimeModule.value.enable"
  106. labelflag="sizeOvertimeModule" empty="true">
  107. <ui-checkbox></ui-checkbox><ui-label></ui-label>
  108. </ui-prop>
  109. <ui-prop type="dump" key="sizeOvertimeModule.value.separateAxes"></ui-prop>
  110. <ui-prop type="dump" key="sizeOvertimeModule.value.size"></ui-prop>
  111. <ui-prop type="dump" key="sizeOvertimeModule.value.x"></ui-prop>
  112. <ui-prop type="dump" key="sizeOvertimeModule.value.y"></ui-prop>
  113. <ui-prop type="dump" key="sizeOvertimeModule.value.z"></ui-prop>
  114. </ui-section>
  115. <ui-section empty="true" key="rotationOvertimeModule"
  116. cache-expand="particle-system-rotationOvertimeModule">
  117. <ui-prop slot="header" no-label class="header" type="dump" key="rotationOvertimeModule.value.enable"
  118. labelflag="rotationOvertimeModule" empty="true">
  119. <ui-checkbox></ui-checkbox><ui-label></ui-label>
  120. </ui-prop>
  121. <ui-prop type="dump" key="rotationOvertimeModule.value.separateAxes"></ui-prop>
  122. <ui-prop type="dump" key="rotationOvertimeModule.value.x"></ui-prop>
  123. <ui-prop type="dump" key="rotationOvertimeModule.value.y"></ui-prop>
  124. <ui-prop type="dump" key="rotationOvertimeModule.value.z"></ui-prop>
  125. </ui-section>
  126. <ui-section key="colorOverLifetimeModule" autoflag="true"
  127. cache-expand="particle-system-colorOverLifetimeModule"></ui-section>
  128. <ui-section key="textureAnimationModule" autoflag="true"
  129. cache-expand="particle-system-textureAnimationModule"></ui-section>
  130. <ui-section type="dump" showflag="!renderer.value.useGPU" key="limitVelocityOvertimeModule"
  131. cache-expand="particle-system-limitVelocityOvertimeModule">
  132. <ui-prop slot="header" no-label class="header" type="dump" key="limitVelocityOvertimeModule.value.enable"
  133. labelflag="limitVelocityOvertimeModule" empty="true">
  134. <ui-checkbox></ui-checkbox><ui-label></ui-label>
  135. </ui-prop>
  136. <ui-prop type="dump" key="limitVelocityOvertimeModule.value.space"></ui-prop>
  137. <ui-prop type="dump" key="limitVelocityOvertimeModule.value.dampen"></ui-prop>
  138. <ui-prop type="dump" key="limitVelocityOvertimeModule.value.separateAxes"></ui-prop>
  139. <ui-prop type="dump" key="limitVelocityOvertimeModule.value.limit"></ui-prop>
  140. <ui-prop type="dump" key="limitVelocityOvertimeModule.value.limitX"></ui-prop>
  141. <ui-prop type="dump" key="limitVelocityOvertimeModule.value.limitY"></ui-prop>
  142. <ui-prop type="dump" key="limitVelocityOvertimeModule.value.limitZ"></ui-prop>
  143. </ui-section>
  144. <ui-section empty="true" showflag="!renderer.value.useGPU" key="trailModule"
  145. cache-expand="particle-system-trailModule">
  146. <ui-prop slot="header" no-label class="header" type="dump" key="trailModule.value.enable" labelflag="trailModule"
  147. empty="true">
  148. <ui-checkbox></ui-checkbox><ui-label></ui-label>
  149. </ui-prop>
  150. <ui-prop type="dump" key="trailModule.value.mode"></ui-prop>
  151. <ui-prop type="dump" key="trailModule.value.lifeTime"></ui-prop>
  152. <ui-prop type="dump" key="trailModule.value.minParticleDistance"></ui-prop>
  153. <ui-prop type="dump" key="trailModule.value.space"></ui-prop>
  154. <ui-prop type="dump" key="trailModule.value.textureMode"></ui-prop>
  155. <ui-prop type="dump" key="trailModule.value.widthFromParticle"></ui-prop>
  156. <ui-prop type="dump" key="trailModule.value.widthRatio"></ui-prop>
  157. <ui-prop type="dump" key="trailModule.value.colorFromParticle"></ui-prop>
  158. <ui-prop type="dump" key="trailModule.value.colorOverTrail"></ui-prop>
  159. <ui-prop type="dump" key="trailModule.value.colorOvertime"></ui-prop>
  160. </ui-section>
  161. <ui-section empty="true" key="renderer" expand>
  162. <div slot="header" style="overflow: hidden;">
  163. <ui-label name style="white-space: nowrap;" value="Renderer"></ui-label>
  164. <ui-label type style="white-space: nowrap;opacity: 0.55;margin-left: 4px;flex:auto;font-weight:normal;" value=": cc.ParticleSystemRenderer"></ui-label>
  165. </div>
  166. <ui-prop type="dump" key="renderer.value.renderMode"></ui-prop>
  167. <ui-prop type="dump" key="renderer.value.velocityScale"></ui-prop>
  168. <ui-prop type="dump" key="renderer.value.lengthScale"></ui-prop>
  169. <ui-prop type="dump" key="renderer.value.mesh"></ui-prop>
  170. <ui-prop class="cpu-material" type="dump" key="renderer.value.cpuMaterial"></ui-prop>
  171. <ui-prop class="trail-material" type="dump" key="renderer.value.trailMaterial"></ui-prop>
  172. <ui-prop class="gpu-material" type="dump" key="renderer.value.gpuMaterial"></ui-prop>
  173. <ui-prop id="use-gpu" type="dump" key="renderer.value.useGPU"></ui-prop>
  174. <ui-prop type="dump" key="renderer.value.alignSpace"></ui-prop>
  175. </ui-section>
  176. </div>
  177. </div>
  178. `;
  179. const excludeList = [
  180. 'duration', 'capacity', 'loop', 'playOnAwake', 'prewarm',
  181. 'simulationSpace', 'simulationSpeed', 'startDelay',
  182. 'startLifetime', 'startColor', 'scaleSpace', 'startSize3D',
  183. 'startSizeX', 'startSizeY', 'startSizeZ', 'startSpeed',
  184. 'startRotation3D', 'startRotationX', 'startRotationY',
  185. 'startRotationZ', 'gravityModifier', 'rateOverTime',
  186. 'rateOverDistance', 'bursts', 'shapeModule',
  187. 'velocityOvertimeModule', 'forceOvertimeModule', 'sizeOvertimeModule',
  188. 'rotationOvertimeModule', 'colorOverLifetimeModule', 'textureAnimationModule',
  189. 'trailModule', 'renderer', 'renderCulling', 'limitVelocityOvertimeModule', 'cullingMode',
  190. 'aabbHalfX', 'aabbHalfY', 'aabbHalfZ', 'noiseModule',
  191. ];
  192. exports.methods = {
  193. getObjectByKey(target, key) {
  194. let params = [];
  195. if (typeof key === 'string') {
  196. params = key.split('.');
  197. } else if (key instanceof Array) {
  198. params = key;
  199. }
  200. if (params.length > 0) {
  201. const value = params.shift();
  202. return this.getObjectByKey(target[value], params);
  203. } else {
  204. return target;
  205. }
  206. },
  207. getEnumName(type, value) {
  208. for (const opt of type.enumList) {
  209. if (opt.value === value) {
  210. return opt.name;
  211. }
  212. }
  213. return String();
  214. },
  215. getEnumObjFromName(type, ...name) {
  216. const enumMap = {};
  217. for (const opt of type.enumList) {
  218. enumMap[opt.name] = {
  219. name: opt.name,
  220. value: opt.value,
  221. };
  222. }
  223. return name.map((value) => enumMap[value]);
  224. },
  225. getShapeTypeEmitFrom(shapeType) {
  226. const shapeTypeName = this.getEnumName(this.dump.value.shapeModule.value.shapeType, shapeType);
  227. let emitEnum = null;
  228. switch (shapeTypeName) {
  229. case 'Box':
  230. emitEnum = this.getEnumObjFromName(this.dump.value.shapeModule.value.emitFrom, 'Volume', 'Shell', 'Edge');
  231. break;
  232. case 'Cone':
  233. emitEnum = this.getEnumObjFromName(this.dump.value.shapeModule.value.emitFrom, 'Base', 'Shell', 'Volume');
  234. break;
  235. case 'Sphere':
  236. emitEnum = this.getEnumObjFromName(this.dump.value.shapeModule.value.emitFrom, 'Volume', 'Shell');
  237. break;
  238. case 'Hemisphere':
  239. emitEnum = this.getEnumObjFromName(this.dump.value.shapeModule.value.emitFrom, 'Volume', 'Shell');
  240. break;
  241. default:
  242. emitEnum = [];
  243. }
  244. return emitEnum;
  245. },
  246. };
  247. const uiElements = {
  248. resetBounds: {
  249. async ready() {
  250. this.$.resetBounds.addEventListener('confirm', async () => {
  251. const nodeDumps = this.dump.value.node.values || [this.dump.value.node.value];
  252. const componentUUIDs = this.dump.value.uuid.values || [this.dump.value.uuid.value];
  253. await Promise.all(componentUUIDs.map(uuid => {
  254. return new Promise((res) => {
  255. Editor.Message.request(propUtils.getMessageProtocolScene(this.$this), 'execute-component-method', {
  256. uuid,
  257. name: '_calculateBounding',
  258. args: [true],
  259. }).then(() => {
  260. Editor.Message.request(propUtils.getMessageProtocolScene(this.$this), 'execute-component-method', {
  261. uuid,
  262. name: 'gizmo.onNodeChanged',
  263. args: [],
  264. });
  265. res();
  266. });
  267. });
  268. }));
  269. nodeDumps.forEach(dump => {
  270. Editor.Message.broadcast('scene:change-node', dump.uuid);
  271. });
  272. });
  273. },
  274. update() {
  275. const isInvalid = propUtils.isMultipleInvalid(this.dump.value.renderCulling);
  276. if (isInvalid || !this.dump.value.renderCulling.value) {
  277. this.$.resetBounds.setAttribute('disabled', true);
  278. } else {
  279. if (this.$.resetBounds.hasAttribute('disabled')) {
  280. this.$.resetBounds.removeAttribute('disabled');
  281. }
  282. }
  283. },
  284. },
  285. uiSections: {
  286. ready() {
  287. this.$.uiSections = this.$this.shadowRoot.querySelectorAll('ui-section');
  288. this.$.uiSections.forEach((element) => {
  289. // expand when checkbox enable
  290. if (element.hasAttribute('autoExpand')) {
  291. element.addEventListener('checkbox-enable', () => {
  292. element.setAttribute('expand', 'expand');
  293. });
  294. }
  295. });
  296. },
  297. update() {
  298. this.$.uiSections.forEach((element) => {
  299. const key = element.getAttribute('key');
  300. const showflag = element.getAttribute('showflag');
  301. const autoflag = element.getAttribute('autoflag');
  302. if (showflag) {
  303. if (typeof showflag === 'string') {
  304. if (showflag.startsWith('!')) {
  305. const dump = this.getObjectByKey(this.dump.value, showflag.slice(1));
  306. const isInvalid = propUtils.isMultipleInvalid(dump);
  307. if (dump.value || isInvalid) {
  308. // continue when don't show
  309. element.style = 'display: none;';
  310. return true;
  311. }
  312. } else {
  313. const dump = this.getObjectByKey(this.dump.value, showflag);
  314. const isInvalid = propUtils.isMultipleInvalid(dump);
  315. if (!dump.value || isInvalid) {
  316. // continue when don't show
  317. element.style = 'display: none;';
  318. return true;
  319. }
  320. }
  321. }
  322. }
  323. element.style = '';
  324. if (autoflag) {
  325. const oldChildren = Array.from(element.children);
  326. const children = [];
  327. const oldCheckbox = element.querySelector('[slot="header"] > ui-checkbox');
  328. if (oldCheckbox) {
  329. oldCheckbox.removeEventListener('change', oldCheckbox.changeEvent);
  330. oldCheckbox.changeEvent = undefined;
  331. }
  332. const header = document.createElement('ui-prop');
  333. header.setAttribute('slot', 'header');
  334. header.setAttribute('no-label', '');
  335. header.setAttribute('type', 'dump');
  336. header.setAttribute('empty', 'true');
  337. header.className = 'header';
  338. const dump = this.getObjectByKey(this.dump.value, key);
  339. header.dump = dump;
  340. const checkbox = document.createElement('ui-checkbox');
  341. checkbox.changeEvent = (event) => {
  342. dump.value.enable.value = event.target.value;
  343. header.dispatch('change-dump');
  344. };
  345. checkbox.addEventListener('change', checkbox.changeEvent);
  346. checkbox.setAttribute('value', dump.value.enable.value);
  347. const label = document.createElement('ui-label');
  348. label.setAttribute('value', propUtils.getName(dump));
  349. label.setAttribute('tooltip', dump.tooltip);
  350. header.replaceChildren(...[checkbox, label]);
  351. children.push(header);
  352. const propMap = dump.value;
  353. for (const propKey in propMap) {
  354. const propDump = propMap[propKey];
  355. if (propKey === 'enable') {
  356. continue;
  357. }
  358. const oldProp = oldChildren.find((child) => child.getAttribute('key') === propKey);
  359. const uiProp = oldProp || document.createElement('ui-prop');
  360. uiProp.setAttribute('type', 'dump');
  361. uiProp.setAttribute('key', propKey);
  362. const isShow = propDump.visible;
  363. if (isShow) {
  364. uiProp.render(propDump);
  365. children.push(uiProp);
  366. }
  367. }
  368. children.sort((a, b) => (a.dump.displayOrder ? a.dump.displayOrder : 0 - b.dump.displayOrder ? b.dump.displayOrder : 0));
  369. children.forEach((newChild, index) => {
  370. const oldChild = oldChildren[index];
  371. if (oldChild === newChild) {
  372. return;
  373. }
  374. if (oldChild) {
  375. oldChild.replaceWith(newChild);
  376. } else {
  377. element.appendChild(newChild);
  378. }
  379. });
  380. while (oldChildren.length > children.length) {
  381. const oldChild = oldChildren.pop();
  382. oldChild.remove();
  383. }
  384. }
  385. });
  386. },
  387. },
  388. showBounds: {
  389. ready() {
  390. this.$.showBounds.addEventListener('change', (event) => {
  391. const componentUUIDs = this.dump.value.uuid.values || [this.dump.value.uuid.value];
  392. componentUUIDs.forEach(uuid => {
  393. Editor.Message.send(propUtils.getMessageProtocolScene(this.$this), 'execute-component-method', {
  394. uuid,
  395. name: 'gizmo.showBoundingBox',
  396. args: [event.target.value],
  397. });
  398. });
  399. });
  400. },
  401. async update() {
  402. if (!this.dump.value.renderCulling.value) {
  403. this.$.showBounds.setAttribute('disabled', true);
  404. } else if (this.$.showBounds.hasAttribute('disabled')) {
  405. this.$.showBounds.removeAttribute('disabled');
  406. }
  407. const componentUUIDs = this.dump.value.uuid.values || [this.dump.value.uuid.value];
  408. const values = await Promise.all(
  409. componentUUIDs.map(
  410. uuid => Editor.Message.request(propUtils.getMessageProtocolScene(this.$this), 'execute-component-method', {
  411. uuid,
  412. name: 'gizmo.isShowBoundingBox',
  413. args: [],
  414. })));
  415. const invalid = values.some(v => v !== values[0]);
  416. this.$.showBounds.invalid = invalid;
  417. this.$.showBounds.value = values[0];
  418. },
  419. },
  420. emitFromSelect: {
  421. ready() {
  422. this.$.emitFromSelect.addEventListener('change', (event) => {
  423. this.dump.value.shapeModule.value.emitFrom.value = event.target.value;
  424. this.$.emitFromSelect.parentNode.dispatch('change-dump');
  425. });
  426. },
  427. update() {
  428. this.$.emitFromSelect.setAttribute('value', this.dump.value.shapeModule.value.emitFrom.value);
  429. const datas = this.getShapeTypeEmitFrom(this.dump.value.shapeModule.value.shapeType.value);
  430. const children = datas.map((data) => {
  431. const child = document.createElement('option');
  432. child.innerHTML = data.name;
  433. child.setAttribute('value', data.value);
  434. return child;
  435. });
  436. this.$.emitFromSelect.replaceChildren(...children);
  437. },
  438. },
  439. baseProps: {
  440. ready() {
  441. this.$.baseProps = this.$this.shadowRoot.querySelectorAll('ui-prop:not(.customProp)');
  442. this.$.baseProps.forEach((element) => {
  443. const key = element.getAttribute('key');
  444. const isEmpty = element.getAttribute('empty');
  445. const isHeader = element.getAttribute('slot') === 'header';
  446. element.addEventListener('change-dump', () => {
  447. uiElements.baseProps.update.call(this, key);
  448. });
  449. if (isEmpty) {
  450. if (isHeader) {
  451. /**
  452. * @type {HTMLInputElement}
  453. */
  454. const checkbox = element.querySelector('ui-checkbox');
  455. if (checkbox) {
  456. checkbox.addEventListener('change', (event) => {
  457. const dump = this.getObjectByKey(this.dump.value, key);
  458. const value = event.target.value;
  459. if (dump.values) {
  460. dump.values = dump.values.map(v => value);
  461. }
  462. dump.value = value;
  463. element.dispatch('change-dump');
  464. if (value) {
  465. // bubbles the event when value is true
  466. const event = new Event('checkbox-enable', { bubbles: true, cancelable: true });
  467. checkbox.dispatchEvent(event);
  468. }
  469. });
  470. }
  471. }
  472. }
  473. });
  474. },
  475. /**
  476. *
  477. * @param {string} [eventInstigatorKey]
  478. */
  479. update(eventInstigatorKey) {
  480. this.$.baseProps.forEach((element) => {
  481. const key = element.getAttribute('key');
  482. const isEmpty = element.getAttribute('empty');
  483. let isShow = !key || this.getObjectByKey(this.dump.value, key).visible;
  484. const isHeader = element.getAttribute('slot') === 'header';
  485. const displayName = element.getAttribute('displayName');
  486. const dump = this.getObjectByKey(this.dump.value, key);
  487. const showflag = element.getAttribute('showflag');
  488. const disableflag = element.getAttribute('disableflag');
  489. let isDisable = false;
  490. if (typeof showflag === 'string') {
  491. // only update the elements relate to eventInstigator
  492. if (eventInstigatorKey) {
  493. if (showflag.startsWith(`!${eventInstigatorKey}`)) {
  494. const dump = this.getObjectByKey(this.dump.value, showflag.slice(1));
  495. const isInvalid = propUtils.isMultipleInvalid(dump);
  496. isShow = isShow && !isInvalid && !dump.value;
  497. } else if (showflag.startsWith(eventInstigatorKey)) {
  498. const dump = this.getObjectByKey(this.dump.value, showflag);
  499. const isInvalid = propUtils.isMultipleInvalid(dump);
  500. isShow = isShow && !isInvalid && dump.value;
  501. } else {
  502. return;
  503. }
  504. } else {
  505. if (showflag.startsWith('!')) {
  506. const dump = this.getObjectByKey(this.dump.value, showflag.slice(1));
  507. const isInvalid = propUtils.isMultipleInvalid(dump);
  508. isShow = isShow && !isInvalid && !dump.value;
  509. } else {
  510. const dump = this.getObjectByKey(this.dump.value, showflag);
  511. const isInvalid = propUtils.isMultipleInvalid(dump);
  512. isShow = isShow && !isInvalid && dump.value;
  513. }
  514. }
  515. } else if (typeof disableflag === 'string') {
  516. // only update the elements relate to eventInstigator
  517. if (eventInstigatorKey) {
  518. const contentSlot = element.querySelector('[slot=content]');
  519. if (!contentSlot) {
  520. return;
  521. }
  522. if (disableflag.startsWith(`!${eventInstigatorKey}`)) {
  523. const dump = this.getObjectByKey(this.dump.value, disableflag.slice(1));
  524. const isInvalid = propUtils.isMultipleInvalid(dump) || !dump.value;
  525. if (isInvalid) {
  526. contentSlot.setAttribute('disabled', true);
  527. } else if (contentSlot.hasAttribute('disabled')) {
  528. contentSlot.removeAttribute('disabled');
  529. }
  530. } else if (disableflag.startsWith(eventInstigatorKey)) {
  531. const dump = this.getObjectByKey(this.dump.value, disableflag);
  532. const isInvalid = propUtils.isMultipleInvalid(dump) || !!dump.value;
  533. if (isInvalid) {
  534. contentSlot.setAttribute('disabled', true);
  535. } else if (contentSlot.hasAttribute('disabled')) {
  536. contentSlot.removeAttribute('disabled');
  537. }
  538. } else {
  539. return;
  540. }
  541. } else {
  542. if (disableflag.startsWith('!')) {
  543. const dump = this.getObjectByKey(this.dump.value, disableflag.slice(1));
  544. const isInvalid = propUtils.isMultipleInvalid(dump);
  545. isDisable = isInvalid || !dump.value;
  546. } else {
  547. const dump = this.getObjectByKey(this.dump.value, disableflag);
  548. const isInvalid = propUtils.isMultipleInvalid(dump);
  549. isDisable = isInvalid || !!dump.value;
  550. }
  551. }
  552. }
  553. else if (eventInstigatorKey) {
  554. // skip all element without showflag
  555. return;
  556. }
  557. dump.displayName = displayName;
  558. if (!isEmpty) {
  559. if (isShow) {
  560. element.render(dump);
  561. }
  562. if (typeof disableflag === 'string') {
  563. const contentSlot = element.querySelector('[slot=content]');
  564. if (contentSlot) {
  565. if (isDisable) {
  566. contentSlot.setAttribute('disabled', true);
  567. } else if (contentSlot.hasAttribute('disabled')) {
  568. contentSlot.removeAttribute('disabled');
  569. }
  570. }
  571. }
  572. } else {
  573. const label = element.querySelector('ui-label');
  574. if (label) {
  575. const labelflag = element.getAttribute('labelflag');
  576. if (labelflag) {
  577. const dump = this.getObjectByKey(this.dump.value, labelflag);
  578. label.setAttribute('value', propUtils.getName(dump));
  579. label.setAttribute('tooltip', dump.tooltip);
  580. }
  581. }
  582. if (isHeader) {
  583. const checkbox = element.querySelector('ui-checkbox');
  584. if (checkbox) {
  585. checkbox.setAttribute('value', dump.value);
  586. checkbox.invalid = propUtils.isMultipleInvalid(dump);
  587. }
  588. }
  589. element.dump = dump;
  590. }
  591. element.style = isShow ? '' : 'display: none;';
  592. });
  593. },
  594. },
  595. customProps: {
  596. update() {
  597. propUtils.updateCustomPropElements(this.$.customProps, excludeList, this.dump, (element, prop) => {
  598. element.className = 'customProp';
  599. if (prop.dump.visible) {
  600. element.render(prop.dump);
  601. }
  602. element.hidden = !prop.dump.visible;
  603. });
  604. },
  605. },
  606. noisePreview: {
  607. async update() {
  608. if (!this.dump?.value?.uuid?.values && !this.dump?.value?.uuid?.value) { return; }
  609. let uuid = this.dump.value.uuid.values ? this.dump.value.uuid.values[0] : this.dump.value.uuid.value;
  610. if (!uuid) { return; }
  611. let data = await Editor.Message.request(propUtils.getMessageProtocolScene(this.$this), 'execute-component-method', {
  612. uuid,
  613. name: 'getNoisePreview',
  614. args: [100, 100],
  615. });
  616. if (data.length === 0) { return; }
  617. data = data.reduce((result, item) => {
  618. const value = item * 255;
  619. const rgba = [value, value, value, 255];
  620. result.push(...rgba);
  621. return result;
  622. }, []);
  623. const imageData = new ImageData(new Uint8ClampedArray(data), 100, 100);
  624. const context = this.$.noisePreview.getContext('2d');
  625. context.putImageData(imageData, 0, 0);
  626. },
  627. },
  628. useGPU: {
  629. changeState(useGPU) {
  630. if (useGPU) {
  631. this.$.cpuMaterial.setAttribute('state-disabled', '');
  632. this.$.trailMaterial.setAttribute('state-disabled', '');
  633. this.$.gpuMaterial.removeAttribute('state-disabled');
  634. } else {
  635. this.$.cpuMaterial.removeAttribute('state-disabled');
  636. this.$.trailMaterial.removeAttribute('state-disabled');
  637. this.$.gpuMaterial.setAttribute('state-disabled', '');
  638. }
  639. },
  640. ready() {
  641. this.$.useGPU.addEventListener('change', (event) => {
  642. uiElements.useGPU.changeState(event.target.value);
  643. });
  644. },
  645. update() {
  646. uiElements.useGPU.changeState.call(this, this.dump.value.renderer.value.useGPU.value);
  647. },
  648. },
  649. };
  650. exports.$ = {
  651. customProps: '#customProps',
  652. emitFromSelect: '#emitFromSelect',
  653. showBounds: '#showBounds',
  654. resetBounds: '#resetBounds',
  655. noisePreview: '#noisePreview',
  656. useGPU: '#use-gpu',
  657. cpuMaterial: '.cpu-material',
  658. gpuMaterial: '.gpu-material',
  659. trailMaterial: '.trail-material',
  660. };
  661. exports.ready = function() {
  662. for (const key in uiElements) {
  663. const element = uiElements[key];
  664. if (typeof element.ready === 'function') {
  665. element.ready.call(this);
  666. }
  667. }
  668. };
  669. exports.update = function(dump) {
  670. this.dump = dump;
  671. for (const key in uiElements) {
  672. const element = uiElements[key];
  673. if (typeof element.update === 'function') {
  674. element.update.call(this);
  675. }
  676. }
  677. };
  678. exports.style = /* css */`
  679. .particle-system-component > .content > .indent {
  680. margin-left: calc(var(--ui-prop-margin-left) + 8px);
  681. }
  682. .particle-system-component ui-section .header ui-checkbox {
  683. margin-right: 4px;
  684. }
  685. .trail-material[state-disabled] {
  686. opacity: 0.5;
  687. }
  688. .gpu-material[state-disabled] {
  689. opacity: 0.5;
  690. }
  691. .cpu-material[state-disabled] {
  692. opacity: 0.5;
  693. }
  694. `;
  695. exports.listeners = {
  696. async 'change-dump'(event) {
  697. const target = event.target;
  698. if (!target) {
  699. return;
  700. }
  701. const dump = event.target.dump;
  702. if (!dump) {
  703. return;
  704. }
  705. // renderMode选择mesh次数
  706. if (dump.path.endsWith('renderer.renderMode') && dump.value === 4) {
  707. Editor.Metrics._trackEventWithTimer({
  708. category: 'particleSystem',
  709. id: 'A100011',
  710. value: 1,
  711. });
  712. }
  713. // 粒子系统其他模块埋点
  714. const trackMap = {
  715. 'noiseModule.enable': 'A100000',
  716. 'shapeModule.enable': 'A100001',
  717. velocityOvertimeModule: 'A100002',
  718. forceOvertimeModule: 'A100003',
  719. 'sizeOvertimeModule.enable': 'A100004',
  720. 'rotationOvertimeModule.enable': 'A100005',
  721. colorOverLifetimeModule: 'A100006',
  722. textureAnimationModule: 'A100007',
  723. 'limitVelocityOvertimeModule.enable':'A100008',
  724. 'trailModule.enable': 'A100009',
  725. };
  726. const dumpKey = Object.keys(trackMap).find(key => dump.path.endsWith(key));
  727. if (!dumpKey) { return; }
  728. const value = dump.type === 'Boolean' ? dump.value : dump.value.enable.value;
  729. if (!value) { return; }
  730. Editor.Metrics._trackEventWithTimer({
  731. category: 'particleSystem',
  732. id: trackMap[dumpKey],
  733. value: 1,
  734. });
  735. },
  736. };