'use strict'; const { updateElementReadonly } = require('../../utils/assets'); const { extname } = require('path'); const { ParseAtlasFile } = require('./parse-atlas'); exports.template = /* html */ `
`; exports.style = /* css */ ` .asset-texture { display: flex; flex: 1; flex-direction: column; padding-right: 4px; } .asset-texture > .content { flex: 1; } .asset-texture > .content .filter-advanced-section, .asset-texture > .content .wrap-advanced-section, .asset-texture > .content .generate-mipmaps-section { margin-left: 1.2em; display: none; } .asset-texture > .content ui-prop.warn { color: var(--color-warn-fill); } .asset-texture > .content > ui-prop.warn ui-select { border-color: var(--color-warn-fill); } .asset-texture > .content > .warn-words { display: none; margin-top: 4px; color: var(--color-warn-fill); } .asset-texture > .preview { position: relative; height: 200px; overflow: hidden; display: flex; justify-content: center; align-items: center; padding: 10px; background: var(--color-normal-fill-emphasis); border: 1px solid var(--color-normal-border-emphasis); } .asset-texture > .preview:hover { border-color: var(--color-warn-fill); } .filter-different { color: var(--color-warn-fill); display: none; } .filter-different .atlas-file-name span { cursor: pointer; text-decoration: underline; } `; exports.$ = { container: '.asset-texture', anisotropyInput: '.anisotropy-input', filterModeSelect: '.filterMode-select', filterAdvancedSection: '.filter-advanced-section', minfilterSelect: '.minfilter-select', magfilterSelect: '.magfilter-select', generateMipmapsSection: '.generate-mipmaps-section', generateMipmapsCheckbox: '.generate-mipmaps-checkbox', mipfilterSelect: '.mipfilter-select', wrapModeProp: '.wrapMode-prop', wrapModeSelect: '.wrapMode-select', wrapAdvancedSection: '.wrap-advanced-section', wrapModeSProp: '.wrapModeS-prop', wrapModeSSelect: '.wrapModeS-select', wrapModeTProp: '.wrapModeT-prop', wrapModeTSelect: '.wrapModeT-select', warnWords: '.warn-words', filterDifferent: '.filter-different', atlasFileName: '.filter-different .atlas-file-name', }; const ModeMap = { filter: { 'Nearest (None)': { minfilter: 'nearest', magfilter: 'nearest', mipfilter: 'none', }, Bilinear: { minfilter: 'linear', magfilter: 'linear', mipfilter: 'none', }, 'Bilinear with Mipmaps': { minfilter: 'linear', magfilter: 'linear', mipfilter: 'nearest', }, 'Trilinear with Mipmaps': { minfilter: 'linear', magfilter: 'linear', mipfilter: 'linear', }, }, wrap: { Repeat: { wrapModeS: 'repeat', wrapModeT: 'repeat', }, Clamp: { wrapModeS: 'clamp-to-edge', wrapModeT: 'clamp-to-edge', }, Mirror: { wrapModeS: 'mirrored-repeat', wrapModeT: 'mirrored-repeat', }, }, }; exports.ModeMap = ModeMap; const Elements = { anisotropy: { ready() { const panel = this; panel.$.anisotropyInput.addEventListener('change', (event) => { panel.userDataList.forEach((userData) => { userData.anisotropy = event.target.value; }); panel.dispatch('change'); }); panel.$.anisotropyInput.addEventListener('confirm', () => { panel.dispatch('snapshot'); }); }, update() { const panel = this; panel.$.anisotropyInput.value = panel.userData.anisotropy; panel.updateInvalid(panel.$.anisotropyInput, 'anisotropy'); updateElementReadonly.call(panel, panel.$.anisotropyInput); }, }, filterMode: { ready() { const panel = this; panel.$.filterModeSelect.addEventListener('change', (event) => { // 根据 filterModeSelect 组合值同步相应的 min/mag/mip 到 userData const value = event.target.value; if (ModeMap.filter[value]) { panel.userDataList.forEach((userData) => { const data = ModeMap.filter[value]; for (const key of Object.keys(data)) { userData[key] = data[key]; } }); // 选择 filterMode 组合选项,不显示自定义项 panel.$.filterAdvancedSection.style.display = 'none'; } else { // 选择 advanced 显示自定义项 panel.$.filterAdvancedSection.style.display = 'block'; } panel.dispatch('change'); }); panel.$.filterModeSelect.addEventListener('confirm', () => { panel.dispatch('snapshot'); }); }, update() { const panel = this; let optionsHtml = ''; // FilterMode 选项 const types = Object.keys(ModeMap.filter).concat('Advanced'); types.forEach((type) => { optionsHtml += ``; }); panel.$.filterModeSelect.innerHTML = optionsHtml; // 匹配 filterModeSelect 值,没有匹配到组合,则为自定义 Advanced let value = 'Advanced'; for (const filterKey of Object.keys(ModeMap.filter)) { const filterItem = ModeMap.filter[filterKey]; let flag = true; for (const key of Object.keys(filterItem)) { if (panel.userData[key] !== filterItem[key]) { flag = false; break; } } if (flag) { value = filterKey; break; } } panel.$.filterModeSelect.value = value; // 更新时判断是否显示自定义项 value === 'Advanced' ? (panel.$.filterAdvancedSection.style.display = 'block') : (panel.$.filterAdvancedSection.style.display = 'none'); panel.updateInvalid(panel.$.filterModeSelect, 'filterMode'); updateElementReadonly.call(panel, panel.$.filterModeSelect); }, }, minfilter: { ready() { const panel = this; panel.$.minfilterSelect.addEventListener('change', (event) => { panel.userDataList.forEach((userData) => { userData.minfilter = event.target.value; }); panel.dispatch('change'); }); panel.$.minfilterSelect.addEventListener('confirm', () => { panel.dispatch('snapshot'); }); }, update() { const panel = this; let optionsHtml = ''; const types = ['nearest', 'linear']; types.forEach((type) => { optionsHtml += ``; }); panel.$.minfilterSelect.innerHTML = optionsHtml; panel.$.minfilterSelect.value = panel.userData.minfilter || 'nearest'; panel.updateInvalid(panel.$.minfilterSelect, 'minfilter'); updateElementReadonly.call(panel, panel.$.minfilterSelect); }, }, magfilter: { ready() { const panel = this; panel.$.magfilterSelect.addEventListener('change', (event) => { panel.userDataList.forEach((userData) => { userData.magfilter = event.target.value; }); panel.dispatch('change'); }); panel.$.magfilterSelect.addEventListener('confirm', () => { panel.dispatch('snapshot'); }); }, update() { const panel = this; let optionsHtml = ''; const types = ['nearest', 'linear']; types.forEach((type) => { optionsHtml += ``; }); panel.$.magfilterSelect.innerHTML = optionsHtml; panel.$.magfilterSelect.value = panel.userData.magfilter || 'nearest'; panel.updateInvalid(panel.$.magfilterSelect, 'magfilter'); updateElementReadonly.call(panel, panel.$.magfilterSelect); }, }, generateMipmaps: { ready() { const panel = this; panel.$.generateMipmapsCheckbox.addEventListener('change', (event) => { panel.userDataList.forEach((userData) => { const value = event.target.value; if (!value) { // 没勾选 生成 mipmaps,不显示 mipfilter 选项 userData.mipfilter = 'none'; panel.$.generateMipmapsSection.style.display = 'none'; } else { panel.$.generateMipmapsSection.style.display = 'block'; // 为空的话默认 nearest if (panel.$.mipfilterSelect.value === 'none') { panel.$.mipfilterSelect.value = 'nearest'; // TODO: 目前 ui-select 通过 .value 修改组件值后没有触发 change 事件,需要手动提交 panel.$.mipfilterSelect.dispatch('change'); } } }); panel.dispatch('change'); }); panel.$.generateMipmapsCheckbox.addEventListener('confirm', () => { panel.dispatch('snapshot'); }); }, update() { const panel = this; panel.$.generateMipmapsCheckbox.value = panel.userData.mipfilter ? panel.userData.mipfilter !== 'none' : false; // 更新时判断是否显示 mipfilter 选项 panel.$.generateMipmapsCheckbox.value ? (panel.$.generateMipmapsSection.style.display = 'block') : (panel.$.generateMipmapsSection.style.display = 'none'); panel.updateInvalid(panel.$.generateMipmapsCheckbox, 'generateMipmaps'); updateElementReadonly.call(panel, panel.$.generateMipmapsCheckbox); }, }, mipfilter: { ready() { const panel = this; panel.$.mipfilterSelect.addEventListener('change', (event) => { panel.userDataList.forEach((userData) => { userData.mipfilter = event.target.value; }); panel.dispatch('change'); }); panel.$.mipfilterSelect.addEventListener('confirm', () => { panel.dispatch('snapshot'); }); }, update() { const panel = this; let optionsHtml = ''; const types = ['nearest', 'linear']; types.forEach((type) => { optionsHtml += ``; }); panel.$.mipfilterSelect.innerHTML = optionsHtml; panel.$.mipfilterSelect.value = panel.userData.mipfilter || 'nearest'; panel.metaList && panel.metaList.forEach((meta) => { Editor.Profile.setTemp('inspector', `${meta.uuid}.texture.mipfilter`, panel.userData.mipfilter); }); panel.updateInvalid(panel.$.mipfilterSelect, 'mipfilter'); updateElementReadonly.call(panel, panel.$.mipfilterSelect); }, }, wrapMode: { ready() { const panel = this; panel.$.wrapModeSelect.addEventListener('change', (event) => { // 根据 wrapModeSelect 组合值同步相应的 wrapModeS/wrapModeT 到 userData const value = event.target.value; // 临时记录用户的修改配置 Editor.Profile.setTemp('inspector', `${this.meta.uuid}.texture.wrapMode`, value, 'default'); if (ModeMap.wrap[value]) { panel.userDataList.forEach((userData) => { const data = ModeMap.wrap[value]; for (const key of Object.keys(data)) { userData[key] = data[key]; } }); panel.$.wrapAdvancedSection.style.display = 'none'; } else { // 选择 advanced 显示自定义项 panel.$.wrapAdvancedSection.style.display = 'block'; } // 校验是否显示警告提示 Elements.warnWords.update.call(panel); panel.dispatch('change'); }); panel.$.wrapModeSelect.addEventListener('confirm', () => { panel.dispatch('snapshot'); }); }, update() { const panel = this; let optionsHtml = ''; // WrapMode 选项 const types = Object.keys(ModeMap.wrap).concat('Advanced'); types.forEach((type) => { optionsHtml += ``; }); panel.$.wrapModeSelect.innerHTML = optionsHtml; // 匹配 wrapModeSelect 值,没有匹配到组合,则为自定义 Advanced let value = 'Advanced'; for (const wrapKey of Object.keys(ModeMap.wrap)) { const wrapItem = ModeMap.wrap[wrapKey]; let flag = true; for (const key of Object.keys(wrapItem)) { if (panel.userData[key] !== wrapItem[key]) { flag = false; break; } } if (flag) { value = wrapKey; break; } } panel.$.wrapModeSelect.value = value; // 更新时需要判断是否显示自定义项 value === 'Advanced' ? (panel.$.wrapAdvancedSection.style.display = 'block') : (panel.$.wrapAdvancedSection.style.display = 'none'); // 校验是否显示警告提示 panel.updateInvalid(panel.$.wrapModeSelect, 'wrapMode'); updateElementReadonly.call(panel, panel.$.wrapModeSelect); }, }, wrapModeS: { ready() { const panel = this; panel.$.wrapModeSSelect.addEventListener('change', (event) => { panel.userDataList.forEach((userData) => { userData.wrapModeS = event.target.value; }); Elements.warnWords.update.call(panel); panel.dispatch('change'); }); panel.$.wrapModeSSelect.addEventListener('confirm', () => { panel.dispatch('snapshot'); }); }, update() { const panel = this; let optionsHtml = ''; const types = { Repeat: 'repeat', Clamp: 'clamp-to-edge', Mirror: 'mirrored-repeat', }; for (const type in types) { optionsHtml += ``; } panel.$.wrapModeSSelect.innerHTML = optionsHtml; panel.$.wrapModeSSelect.value = panel.userData.wrapModeS || 'repeat'; panel.updateInvalid(panel.$.wrapModeSSelect, 'wrapModeS'); updateElementReadonly.call(panel, panel.$.wrapModeSSelect); }, }, wrapModeT: { ready() { const panel = this; panel.$.wrapModeTSelect.addEventListener('change', (event) => { panel.userDataList.forEach((userData) => { userData.wrapModeT = event.target.value; }); Elements.warnWords.update.call(panel); panel.dispatch('change'); }); panel.$.wrapModeTSelect.addEventListener('confirm', () => { panel.dispatch('snapshot'); }); }, update() { const panel = this; let optionsHtml = ''; const types = { Repeat: 'repeat', Clamp: 'clamp-to-edge', Mirror: 'mirrored-repeat', }; for (const type in types) { optionsHtml += ``; } panel.$.wrapModeTSelect.innerHTML = optionsHtml; panel.$.wrapModeTSelect.value = panel.userData.wrapModeT || 'repeat'; panel.updateInvalid(panel.$.wrapModeTSelect, 'wrapModeT'); updateElementReadonly.call(panel, panel.$.wrapModeTSelect); }, }, /** * Condition check: whether the width and height of the image is a power of 2 * A warning message is required if the wrap mode value is repeat. */ warnWords: { ready() { this.$.image = document.createElement('ui-image'); this.$.image.$img.addEventListener('load', () => { Elements.warnWords.update.call(this); }); }, update() { const panel = this; this.$.image.value = panel.asset.uuid; let isUnlegalWrapModeT = false; let isUnlegalWrapModeS = false; if (panel.$.image.$img.src) { const { naturalWidth, naturalHeight } = panel.$.image.$img; const { wrapModeT, wrapModeS } = panel.userData; // Determine the power of 2 algorithm: (number & number - 1) === 0 const isUnlegal = naturalWidth & (naturalWidth - 1) || naturalHeight & (naturalHeight - 1); isUnlegalWrapModeT = isUnlegal && wrapModeT === 'repeat'; isUnlegalWrapModeS = isUnlegal && wrapModeS === 'repeat'; } if (isUnlegalWrapModeT || isUnlegalWrapModeS) { this.$.warnWords.style.display = 'block'; this.$.wrapModeSProp.classList.add('warn'); this.$.wrapModeTProp.classList.add('warn'); this.$.wrapModeProp.classList.add('warn'); } else { this.$.warnWords.style.display = 'none'; this.$.wrapModeSProp.classList.remove('warn'); this.$.wrapModeTProp.classList.remove('warn'); this.$.wrapModeProp.classList.remove('warn'); } }, }, checkAtlasFileConfig: { async ready() { this.$.atlasFileName.addEventListener( 'click', () => { Editor.Message.send('assets', 'twinkle', this.$.atlasFileName.getAttribute('data-uuid')); }, false, ); }, async update() { try { if (!Array.isArray(this.parentAssetList)) { this.$.filterDifferent.style.display = 'none'; return; } const parentPath = this.parentAssetList[0].path.replace(/\/[^/]+$/, '/*'); let assets = await Editor.Message.request('asset-db', 'query-assets', { pattern: parentPath, ccType: 'cc.Asset' }); assets = assets.filter((v) => extname(v.file) === '.atlas'); let matchedAsset; let matchedAtlasJson; for (const asset of assets) { const json = await new ParseAtlasFile().parse(asset.file); // the asset.displayName is not a full name, miss the extname // so, we should use RegExp to get the extname const regStr = `${this.asset.displayName}(.*?)/`; const match = this.asset.url.match(new RegExp(regStr)); const imageExtname = match ? match[1] : '.png'; const imageFullName = this.asset.displayName + imageExtname; if (json[imageFullName]) { matchedAsset = asset; matchedAtlasJson = json[imageFullName]; break; } } if (matchedAsset) { let atlasFileFilter = matchedAtlasJson.filter; if (Array.isArray(atlasFileFilter)) { atlasFileFilter = atlasFileFilter.join(); } else if (atlasFileFilter) { atlasFileFilter = `${atlasFileFilter},${atlasFileFilter}`; } const userDataFilter = `${this.meta.userData.minfilter},${this.meta.userData.magfilter}`; if (atlasFileFilter && atlasFileFilter.toLowerCase() !== userDataFilter.toLowerCase()) { this.$.filterDifferent.style.display = 'block'; const tipHtml = Editor.I18n.t('ENGINE.assets.texture.filterDiffenent').replace( '{atlasFile}', `${matchedAsset.name}`, ); this.$.atlasFileName.innerHTML = tipHtml; this.$.atlasFileName.setAttribute('data-uuid', matchedAsset.uuid); } else { this.$.filterDifferent.style.display = 'none'; } } } catch (error) { this.$.filterDifferent.style.display = 'none'; console.warn('parse atlas file error:', error); } }, }, }; exports.Elements = Elements; exports.methods = { /** * Update whether a data is editable in multi-select state * 多选时候,以选中的第一个作为标准,如果选中其中有一个数据不一致,则该选项无效,需要重新选择 */ updateInvalid(element, prop) { // filterMode、 wrapMode 和 generateMipmaps 需要拆解进行判断 let invalid; switch (prop) { case 'filterMode': invalid = this.userDataList.some((userData) => { for (const key of Object.keys(ModeMap.filter.Bilinear)) { if (userData[key] !== this.userData[key]) { return true; } } return false; }); break; case 'wrapMode': invalid = this.userDataList.some((userData) => { for (const key of Object.keys(ModeMap.wrap.Repeat)) { if (userData[key] !== this.userData[key]) { return true; } } return false; }); break; case 'generateMipmaps': invalid = this.userDataList.some((userData) => { return userData['mipfilter'] !== this.userData['mipfilter']; }); break; default: invalid = this.userDataList.some((userData) => { return userData[prop] !== this.userData[prop]; }); break; } element.invalid = invalid; }, }; exports.ready = function () { for (const prop in Elements) { const element = Elements[prop]; if (element.ready) { element.ready.call(this); } } };