mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-02 20:44:27 +00:00
yapf.js: decodeTexture (container, yapf-sum checks, code books with context decoding, 16-bit / float planes, GPU block tiles), BC1-BC7 / BC6H block decoding and textureToRGBA for display. Matches the C library on 417 generated files (surfaces bit-exact) and survives fuzzing with clean YapfErrors; fixtures in bindings/js/test-data keep it checked. VS Code preview: pickers for mip, face, layer, frame and slice; storage, normal-map convention and version in the info bar. Figma import: textures converted to 8-bit, 8-bit images stay exact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
141 lines
6.4 KiB
JavaScript
141 lines
6.4 KiB
JavaScript
// Webview side of the YAPF preview: decode, draw, zoom, surface pickers, info.
|
||
(function () {
|
||
const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null;
|
||
const canvas = document.getElementById('canvas');
|
||
const info = document.getElementById('info');
|
||
const controls = document.getElementById('controls');
|
||
const errorBox = document.getElementById('error');
|
||
const PIXEL_HINTS = ['RGBA8', 'RGB8', 'RG8', 'R8', 'SRGB8_A8', 'SRGB8', 'RGBA16', 'RGB16', 'RG16', 'R16',
|
||
'RGBA16F', 'RGB16F', 'RG16F', 'R16F', 'RGBA32F', 'RGB32F', 'RG32F', 'R32F'];
|
||
const BLOCKS = { 0x40: 'BC1', 0x41: 'BC2', 0x42: 'BC3', 0x43: 'BC4', 0x44: 'BC4 snorm', 0x45: 'BC5',
|
||
0x46: 'BC5 snorm', 0x47: 'BC6H', 0x48: 'BC6H signed', 0x49: 'BC7', 0x50: 'ETC2 RGB',
|
||
0x51: 'ETC2 RGB A1', 0x52: 'ETC2 RGBA', 0x53: 'EAC R11', 0x54: 'EAC R11 snorm',
|
||
0x55: 'EAC RG11', 0x56: 'EAC RG11 snorm' };
|
||
['4×4', '5×4', '5×5', '6×5', '6×6', '8×5', '8×6', '8×8', '10×5', '10×6', '10×8', '10×10', '12×10', '12×12']
|
||
.forEach((n, i) => { BLOCKS[0x60 + i] = 'ASTC ' + n; });
|
||
const CH_NAMES = ['', 'Gray', 'Gray + Alpha', 'RGB', 'RGBA'];
|
||
const SAMPLES = ['8-bit', '16-bit', 'half float', 'float'];
|
||
const FACES = ['+X', '−X', '+Y', '−Y', '+Z', '−Z'];
|
||
|
||
let tex = null, fit = true, zoom = 1;
|
||
const sel = { mip: 0, face: 0, layer: 0, frame: 0, slice: 0 };
|
||
|
||
function kb(n) { return n < 1024 * 1024 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(2) + ' MB'; }
|
||
|
||
function mipDims(m) {
|
||
return [Math.max(1, tex.width >>> m), Math.max(1, tex.height >>> m),
|
||
tex.shape === 2 ? Math.max(1, tex.depth >>> m) : 1];
|
||
}
|
||
|
||
function draw() {
|
||
const s = YAPF.surfaceIndex(tex, sel.frame, sel.layer, sel.face, sel.mip);
|
||
sel.slice = Math.min(sel.slice, mipDims(sel.mip)[2] - 1);
|
||
try {
|
||
const view = YAPF.textureToRGBA(tex, s, sel.slice);
|
||
canvas.width = view.width; canvas.height = view.height;
|
||
canvas.getContext('2d').putImageData(new ImageData(view.rgba, view.width, view.height), 0, 0);
|
||
errorBox.hidden = true;
|
||
} catch (e) {
|
||
errorBox.hidden = false;
|
||
errorBox.textContent = e.message;
|
||
}
|
||
layout();
|
||
}
|
||
|
||
function layout() {
|
||
if (!tex) return;
|
||
const w = canvas.width, h = canvas.height;
|
||
const stage = document.getElementById('stage');
|
||
const scale = fit ? Math.min(1, (stage.clientWidth - 32) / w, (stage.clientHeight - 32) / h) : zoom;
|
||
canvas.style.width = Math.max(1, Math.round(w * scale)) + 'px';
|
||
canvas.style.height = Math.max(1, Math.round(h * scale)) + 'px';
|
||
canvas.classList.toggle('pixelated', scale >= 2);
|
||
document.body.classList.toggle('fit', fit);
|
||
stage.title = fit ? 'Click for 100%, Ctrl+scroll to zoom' : 'Click to fit';
|
||
}
|
||
|
||
function picker(key, count, label) {
|
||
if (count <= 1) return;
|
||
const el = document.createElement('select');
|
||
for (let i = 0; i < count; i++) {
|
||
const o = document.createElement('option');
|
||
o.value = i;
|
||
o.textContent = label(i);
|
||
el.appendChild(o);
|
||
}
|
||
sel[key] = Math.min(sel[key], count - 1);
|
||
el.value = sel[key];
|
||
el.onchange = () => { sel[key] = +el.value; if (key === 'mip') buildControls(); draw(); };
|
||
controls.appendChild(el);
|
||
}
|
||
|
||
function buildControls() {
|
||
controls.textContent = '';
|
||
picker('mip', tex.mipLevels, (m) => { const [w, h, d] = mipDims(m); return 'Mip ' + m + ' (' + w + '×' + h + (d > 1 ? '×' + d : '') + ')'; });
|
||
picker('face', tex.faces, (f) => 'Face ' + FACES[f]);
|
||
picker('layer', tex.layers, (l) => 'Layer ' + l);
|
||
picker('frame', tex.frames, (f) => 'Frame ' + (f + 1) + (tex.frameUs[f] ? ' (' + Math.round(tex.frameUs[f] / 1000) + ' ms)' : ''));
|
||
picker('slice', mipDims(sel.mip)[2], (z) => 'Slice ' + z);
|
||
}
|
||
|
||
function show(name, bytes) {
|
||
errorBox.hidden = true;
|
||
const t = performance.now();
|
||
try {
|
||
tex = YAPF.decodeTexture(bytes);
|
||
} catch (e) {
|
||
tex = null;
|
||
errorBox.hidden = false;
|
||
errorBox.textContent = name + ': ' + e.message.replace(/^YAPF: /, '');
|
||
info.textContent = '';
|
||
controls.textContent = '';
|
||
return;
|
||
}
|
||
const ms = performance.now() - t;
|
||
let raw = 0;
|
||
for (let m = 0; m < tex.mipLevels; m++) {
|
||
const [w, h, d] = mipDims(m);
|
||
raw += w * h * d * (tex.payload ? 4 : tex.channels * [1, 2, 2, 4][tex.sample]);
|
||
}
|
||
raw *= tex.frames * tex.layers * tex.faces;
|
||
const [w0, h0, d0] = mipDims(0);
|
||
const shape = tex.shape === 1 ? (tex.layers > 1 ? 'cube array of ' + tex.layers : 'cubemap')
|
||
: tex.shape === 2 ? 'volume' : tex.layers > 1 ? 'array of ' + tex.layers : '';
|
||
const storage = tex.payload === 1 ? 'GPU ' + (BLOCKS[tex.format] || '?')
|
||
: CH_NAMES[tex.channels] + ' ' + SAMPLES[tex.sample] + ', GPU ' + (PIXEL_HINTS[tex.format] || '?');
|
||
const parts = [
|
||
w0 + ' × ' + h0 + (d0 > 1 ? ' × ' + d0 : ''),
|
||
shape,
|
||
tex.frames > 1 ? tex.frames + ' frames' : '',
|
||
storage,
|
||
(tex.flags & 2 ? 'sRGB' : 'linear') + (tex.flags & 1 ? ', premultiplied' : ''),
|
||
tex.normal ? 'normal map (' + ((tex.normal & 3) === 2 ? 'DirectX' : (tex.normal & 3) === 1 ? 'OpenGL' : '?') + ')' : '',
|
||
tex.version === 2 ? 'YAPF 2' : 'YAPF 1',
|
||
kb(bytes.length) + (tex.payload ? '' : ' (' + (100 * bytes.length / raw).toFixed(1) + '% of raw)'),
|
||
'decoded in ' + ms.toFixed(1) + ' ms',
|
||
];
|
||
const author = tex.notes.find((n) => n.tag === 'AUTH');
|
||
if (author) parts.push('by ' + new TextDecoder().decode(author.data));
|
||
info.textContent = parts.filter(Boolean).join(' · ');
|
||
buildControls();
|
||
draw();
|
||
}
|
||
|
||
document.getElementById('stage').addEventListener('click', () => { fit = !fit; zoom = 1; layout(); });
|
||
document.getElementById('stage').addEventListener('wheel', (e) => {
|
||
if (!e.ctrlKey || !tex) return;
|
||
e.preventDefault();
|
||
fit = false;
|
||
zoom = Math.min(64, Math.max(1 / 64, zoom * (e.deltaY < 0 ? 1.25 : 0.8)));
|
||
layout();
|
||
}, { passive: false });
|
||
window.addEventListener('resize', layout);
|
||
|
||
window.addEventListener('message', (e) => {
|
||
const m = e.data;
|
||
if (m.type === 'file') show(m.name, m.bytes instanceof Uint8Array ? m.bytes : new Uint8Array(m.bytes));
|
||
if (m.type === 'error') { errorBox.hidden = false; errorBox.textContent = m.message; }
|
||
});
|
||
if (vscode) vscode.postMessage({ type: 'ready' });
|
||
window.yapfPreviewShow = show; // used by the browser test page
|
||
})();
|