You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
372 lines
16 KiB
372 lines
16 KiB
(function (global) {
|
|
'use strict';
|
|
|
|
/**
|
|
* Asset inventory from cc.assetManager only.
|
|
* @constructor
|
|
*/
|
|
function AssetsPanel() {
|
|
this.el = null;
|
|
this._cc = null;
|
|
this._rows = [];
|
|
this._bundleFilter = '';
|
|
this._typeFilter = '';
|
|
this._query = '';
|
|
this._sort = 'mem';
|
|
this._selected = null;
|
|
this._timer = 0;
|
|
}
|
|
|
|
/** @param {*} cc */
|
|
AssetsPanel.prototype.bindEngine = function (cc) {
|
|
this._cc = cc;
|
|
var self = this;
|
|
if (this._timer) clearInterval(this._timer);
|
|
this._timer = setInterval(function () {
|
|
if (!self.el || self.el.offsetParent === null) return;
|
|
self.refresh();
|
|
}, 2000);
|
|
this.refresh();
|
|
};
|
|
|
|
AssetsPanel.prototype.mount = function () {
|
|
var self = this;
|
|
var root = document.createElement('div');
|
|
root.className = 'pw-panel-inner';
|
|
root.style.height = '100%';
|
|
root.style.display = 'flex';
|
|
root.style.flexDirection = 'column';
|
|
root.innerHTML =
|
|
'<div class="pw-panel-head">' +
|
|
'<span class="pw-panel-title">资源</span>' +
|
|
'<div class="pw-panel-tools">' +
|
|
'<select id="pw-asset-bundle" class="pw-select"><option value="">分包:全部</option></select>' +
|
|
'<select id="pw-asset-type" class="pw-select">' +
|
|
'<option value="">类型:全部</option>' +
|
|
'<option value="Texture2D">纹理 Texture2D</option>' +
|
|
'<option value="SpriteFrame">图集帧 SpriteFrame</option>' +
|
|
'<option value="Prefab">预制体 Prefab</option>' +
|
|
'<option value="sp.SkeletonData">Spine</option>' +
|
|
'<option value="AudioClip">音频</option>' +
|
|
'<option value="JsonAsset">JSON</option>' +
|
|
'<option value="Other">其他</option>' +
|
|
'</select>' +
|
|
'<input id="pw-asset-q" class="pw-input" placeholder="搜索" style="width:90px" />' +
|
|
'<select id="pw-asset-sort" class="pw-select">' +
|
|
'<option value="mem">按内存↓</option>' +
|
|
'<option value="name">按名称</option>' +
|
|
'<option value="ref">按引用↓</option>' +
|
|
'<option value="type">按类型</option>' +
|
|
'</select>' +
|
|
'<button type="button" id="pw-asset-refresh" class="pw-btn">刷新</button>' +
|
|
'</div></div>' +
|
|
'<div class="pw-panel-body">' +
|
|
'<div id="pw-asset-summary" class="pw-summary"></div>' +
|
|
'<div id="pw-asset-bundles" class="pw-muted" style="margin-bottom:6px"></div>' +
|
|
'<div style="display:flex;gap:8px;min-height:0;flex:1">' +
|
|
'<div style="flex:1;overflow:auto;min-width:0"><table class="pw-table" id="pw-asset-table"><thead><tr>' +
|
|
'<th>名称</th><th>类型</th><th>分包</th><th>内存</th><th>引用</th>' +
|
|
'</tr></thead><tbody></tbody></table></div>' +
|
|
'<div style="width:180px;flex-shrink:0"><div class="pw-muted">预览</div><div class="pw-preview-box" id="pw-asset-preview"></div><div id="pw-asset-meta" class="pw-muted" style="margin-top:4px"></div></div>' +
|
|
'</div></div>';
|
|
this.el = root;
|
|
root.querySelector('#pw-asset-bundle').addEventListener('change', function (ev) {
|
|
self._bundleFilter = ev.target.value;
|
|
self.renderList();
|
|
});
|
|
root.querySelector('#pw-asset-type').addEventListener('change', function (ev) {
|
|
self._typeFilter = ev.target.value;
|
|
self.renderList();
|
|
});
|
|
root.querySelector('#pw-asset-q').addEventListener('input', function (ev) {
|
|
self._query = (ev.target.value || '').toLowerCase();
|
|
self.renderList();
|
|
});
|
|
root.querySelector('#pw-asset-sort').addEventListener('change', function (ev) {
|
|
self._sort = ev.target.value;
|
|
self.renderList();
|
|
});
|
|
root.querySelector('#pw-asset-refresh').addEventListener('click', function () { self.refresh(); });
|
|
return root;
|
|
};
|
|
|
|
AssetsPanel.prototype.refresh = function () {
|
|
var cc = this._cc;
|
|
if (!cc || !cc.assetManager) {
|
|
if (this.el) {
|
|
var sum = this.el.querySelector('#pw-asset-summary');
|
|
if (sum) sum.innerHTML = '<span class="pw-warn">引擎未就绪</span>';
|
|
}
|
|
return;
|
|
}
|
|
this._rows = this._collect(cc);
|
|
this._fillBundleSelect();
|
|
this._renderSummary(cc);
|
|
this.renderList();
|
|
};
|
|
|
|
AssetsPanel.prototype._collect = function (cc) {
|
|
var rows = [];
|
|
var seen = {};
|
|
var am = cc.assetManager;
|
|
|
|
function typeName(asset) {
|
|
if (!asset || !asset.constructor) return 'Other';
|
|
var n = asset.constructor.name || 'Other';
|
|
if (n.indexOf('SkeletonData') >= 0) return 'sp.SkeletonData';
|
|
return n;
|
|
}
|
|
|
|
function estimate(asset) {
|
|
try {
|
|
if (!asset) return null;
|
|
var n = asset.constructor && asset.constructor.name;
|
|
if (n === 'Texture2D' || (asset.width && asset.height && asset.getPixelFormat)) {
|
|
var w = asset.width || 0;
|
|
var h = asset.height || 0;
|
|
var bpp = 4;
|
|
var mips = asset.mipmapLevel > 1 ? 1.33 : 1;
|
|
return Math.round(w * h * bpp * mips);
|
|
}
|
|
if (n === 'ImageAsset' && asset.width && asset.height) {
|
|
return asset.width * asset.height * 4;
|
|
}
|
|
if (n === 'SpriteFrame' && asset.texture) {
|
|
return estimate(asset.texture);
|
|
}
|
|
if (n === 'AudioClip') {
|
|
if (asset._nativeAsset && asset._nativeAsset.byteLength) return asset._nativeAsset.byteLength;
|
|
if (typeof asset.getDuration === 'function') return Math.round(asset.getDuration() * 176400);
|
|
return null;
|
|
}
|
|
if (n === 'JsonAsset' && asset.json) {
|
|
return JSON.stringify(asset.json).length * 2;
|
|
}
|
|
if (asset.nativeUrl && asset._nativeAsset && asset._nativeAsset.byteLength) {
|
|
return asset._nativeAsset.byteLength;
|
|
}
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function pushAsset(asset, bundleName) {
|
|
if (!asset || !asset.isValid) return;
|
|
var uuid = asset.uuid || asset._uuid || '';
|
|
var key = uuid || (bundleName + ':' + (asset.name || '') + ':' + typeName(asset));
|
|
if (seen[key]) return;
|
|
seen[key] = true;
|
|
var bytes = estimate(asset);
|
|
rows.push({
|
|
name: asset.name || uuid || '(asset)',
|
|
uuid: uuid,
|
|
type: typeName(asset),
|
|
bundle: bundleName || '',
|
|
ref: typeof asset.refCount === 'number' ? asset.refCount : 0,
|
|
bytes: bytes,
|
|
asset: asset,
|
|
});
|
|
}
|
|
|
|
try {
|
|
am.assets.forEach(function (asset) {
|
|
var bundleName = '';
|
|
var uuid = asset && (asset.uuid || asset._uuid);
|
|
try {
|
|
if (uuid && am.bundles) {
|
|
am.bundles.forEach(function (bundle, name) {
|
|
if (bundleName) return;
|
|
if (bundle.getAssetInfo && bundle.getAssetInfo(uuid)) {
|
|
bundleName = name || bundle.name || '';
|
|
}
|
|
});
|
|
}
|
|
} catch (e2) { /* ignore */ }
|
|
pushAsset(asset, bundleName);
|
|
});
|
|
} catch (e3) {
|
|
console.warn('[Workbench] assets enumerate', e3);
|
|
}
|
|
|
|
return rows;
|
|
};
|
|
|
|
AssetsPanel.prototype._fillBundleSelect = function () {
|
|
var sel = this.el.querySelector('#pw-asset-bundle');
|
|
if (!sel) return;
|
|
var cur = this._bundleFilter;
|
|
var names = {};
|
|
for (var i = 0; i < this._rows.length; i++) {
|
|
if (this._rows[i].bundle) names[this._rows[i].bundle] = true;
|
|
}
|
|
var html = '<option value="">分包:全部</option>';
|
|
var keys = Object.keys(names).sort();
|
|
for (var k = 0; k < keys.length; k++) {
|
|
html += '<option value="' + escapeAttr(keys[k]) + '"' + (keys[k] === cur ? ' selected' : '') + '>' + escapeHtml(keys[k]) + '</option>';
|
|
}
|
|
sel.innerHTML = html;
|
|
};
|
|
|
|
AssetsPanel.prototype._renderSummary = function (cc) {
|
|
var sum = this.el.querySelector('#pw-asset-summary');
|
|
var bun = this.el.querySelector('#pw-asset-bundles');
|
|
var total = 0;
|
|
var est = 0;
|
|
var na = 0;
|
|
var byBundle = {};
|
|
for (var i = 0; i < this._rows.length; i++) {
|
|
var r = this._rows[i];
|
|
total++;
|
|
if (r.bytes == null) na++;
|
|
else est += r.bytes;
|
|
var b = r.bundle || '(none)';
|
|
if (!byBundle[b]) byBundle[b] = { count: 0, bytes: 0, na: 0 };
|
|
byBundle[b].count++;
|
|
if (r.bytes == null) byBundle[b].na++;
|
|
else byBundle[b].bytes += r.bytes;
|
|
}
|
|
var tex = 0;
|
|
var buf = 0;
|
|
if (cc.profiler && cc.profiler.stats) {
|
|
if (cc.profiler.stats.textureMemory) tex = cc.profiler.stats.textureMemory.counter.value;
|
|
if (cc.profiler.stats.bufferMemory) buf = cc.profiler.stats.bufferMemory.counter.value;
|
|
}
|
|
var bundleCount = 0;
|
|
try { cc.assetManager.bundles.forEach(function () { bundleCount++; }); } catch (e) { /* */ }
|
|
if (sum) {
|
|
sum.innerHTML =
|
|
'<span>分包 <b>' + bundleCount + '</b></span>' +
|
|
'<span>资源 <b>' + total + '</b></span>' +
|
|
'<span>估算 <b>' + formatBytes(est) + '</b>' + (na ? ' <span class="pw-warn">(' + na + ' 无法估算)</span>' : '') + '</span>' +
|
|
'<span>纹理显存 <b>' + (Math.round(tex * 100) / 100) + 'MB</b></span>' +
|
|
'<span>缓冲显存 <b>' + (Math.round(buf * 100) / 100) + 'MB</b></span>';
|
|
}
|
|
if (bun) {
|
|
var parts = [];
|
|
var bkeys = Object.keys(byBundle).sort();
|
|
for (var j = 0; j < bkeys.length; j++) {
|
|
var info = byBundle[bkeys[j]];
|
|
parts.push(bkeys[j] + ': ' + info.count + ' / ' + formatBytes(info.bytes) + (info.na ? ' (' + info.na + ' n/a)' : ''));
|
|
}
|
|
bun.textContent = parts.join(' · ');
|
|
}
|
|
};
|
|
|
|
AssetsPanel.prototype.renderList = function () {
|
|
var tbody = this.el.querySelector('#pw-asset-table tbody');
|
|
if (!tbody) return;
|
|
var rows = this._rows.slice();
|
|
var self = this;
|
|
rows = rows.filter(function (r) {
|
|
if (self._bundleFilter && r.bundle !== self._bundleFilter) return false;
|
|
if (self._typeFilter) {
|
|
if (self._typeFilter === 'Other') {
|
|
var known = { Texture2D: 1, SpriteFrame: 1, Prefab: 1, 'sp.SkeletonData': 1, AudioClip: 1, JsonAsset: 1 };
|
|
if (known[r.type]) return false;
|
|
} else if (r.type !== self._typeFilter && r.type.indexOf(self._typeFilter) < 0) {
|
|
return false;
|
|
}
|
|
}
|
|
if (self._query && (r.name + ' ' + r.uuid + ' ' + r.type).toLowerCase().indexOf(self._query) < 0) return false;
|
|
return true;
|
|
});
|
|
rows.sort(function (a, b) {
|
|
if (self._sort === 'name') return a.name.localeCompare(b.name);
|
|
if (self._sort === 'ref') return b.ref - a.ref;
|
|
if (self._sort === 'type') return a.type.localeCompare(b.type);
|
|
var ab = a.bytes == null ? -1 : a.bytes;
|
|
var bb = b.bytes == null ? -1 : b.bytes;
|
|
return bb - ab;
|
|
});
|
|
|
|
var html = [];
|
|
var limit = Math.min(rows.length, 400);
|
|
for (var i = 0; i < limit; i++) {
|
|
var r = rows[i];
|
|
var sel = self._selected && self._selected.uuid === r.uuid ? ' selected' : '';
|
|
html.push(
|
|
'<tr class="' + sel + '" data-idx="' + i + '">' +
|
|
'<td title="' + escapeAttr(r.uuid) + '">' + escapeHtml(r.name) + '</td>' +
|
|
'<td>' + escapeHtml(r.type) + '</td>' +
|
|
'<td>' + escapeHtml(r.bundle || '-') + '</td>' +
|
|
'<td class="pw-num">' + (r.bytes == null ? '<span class="pw-warn">无法估算</span>' : formatBytes(r.bytes)) + '</td>' +
|
|
'<td class="pw-num">' + r.ref + '</td></tr>'
|
|
);
|
|
}
|
|
if (rows.length > limit) html.push('<tr><td colspan="5" class="pw-muted">仅显示前 ' + limit + ' / ' + rows.length + '</td></tr>');
|
|
tbody.innerHTML = html.join('') || '<tr><td colspan="5" class="pw-muted">暂无资源</td></tr>';
|
|
this._filtered = rows;
|
|
var trs = tbody.querySelectorAll('tr[data-idx]');
|
|
for (var t = 0; t < trs.length; t++) {
|
|
trs[t].addEventListener('click', function (ev) {
|
|
var idx = parseInt(ev.currentTarget.getAttribute('data-idx'), 10);
|
|
self._selected = self._filtered[idx];
|
|
self.renderList();
|
|
self._preview(self._selected);
|
|
});
|
|
}
|
|
};
|
|
|
|
AssetsPanel.prototype._preview = function (row) {
|
|
var box = this.el.querySelector('#pw-asset-preview');
|
|
var meta = this.el.querySelector('#pw-asset-meta');
|
|
if (!box || !meta) return;
|
|
box.innerHTML = '';
|
|
if (!row || !row.asset) {
|
|
meta.textContent = '';
|
|
return;
|
|
}
|
|
meta.textContent = row.type + ' | ' + (row.bytes == null ? '无法估算' : formatBytes(row.bytes)) + ' | ' + (row.uuid || '');
|
|
var asset = row.asset;
|
|
var tex = null;
|
|
if (row.type === 'Texture2D') tex = asset;
|
|
else if (row.type === 'SpriteFrame' && asset.texture) tex = asset.texture;
|
|
if (tex && tex.width && tex.height) {
|
|
var url = tex.nativeUrl || (asset && asset.nativeUrl) || '';
|
|
if (url) {
|
|
var img = document.createElement('img');
|
|
img.alt = row.name;
|
|
img.style.maxWidth = '100%';
|
|
img.style.maxHeight = '160px';
|
|
img.src = url;
|
|
img.onerror = function () {
|
|
box.innerHTML = '<span class="pw-muted">' + tex.width + 'x' + tex.height + '</span>';
|
|
};
|
|
box.appendChild(img);
|
|
} else {
|
|
box.innerHTML = '<span class="pw-muted">' + tex.width + 'x' + tex.height + '(无预览地址)</span>';
|
|
}
|
|
meta.textContent += ' | ' + tex.width + 'x' + tex.height + '(估算)';
|
|
return;
|
|
}
|
|
if (row.type === 'JsonAsset' && asset.json) {
|
|
var pre = document.createElement('pre');
|
|
pre.style.cssText = 'max-height:160px;overflow:auto;margin:0;font-size:10px;text-align:left;width:100%';
|
|
var text = JSON.stringify(asset.json, null, 0);
|
|
pre.textContent = text.length > 800 ? text.slice(0, 800) + '…' : text;
|
|
box.appendChild(pre);
|
|
return;
|
|
}
|
|
if (row.type === 'AudioClip') {
|
|
box.innerHTML = '<span class="pw-muted">Audio ' + (typeof asset.getDuration === 'function' ? asset.getDuration().toFixed(2) + 's' : '') + '</span>';
|
|
return;
|
|
}
|
|
box.innerHTML = '<span class="pw-muted">无法预览</span>';
|
|
};
|
|
|
|
function formatBytes(n) {
|
|
if (n == null || isNaN(n)) return '无法估算';
|
|
if (n < 1024) return n + 'B';
|
|
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + 'KB';
|
|
return (n / (1024 * 1024)).toFixed(2) + 'MB';
|
|
}
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
function escapeAttr(s) {
|
|
return escapeHtml(s).replace(/"/g, '"');
|
|
}
|
|
|
|
global.PWAssetsPanel = AssetsPanel;
|
|
})(window);
|
|
|