mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-03 04:54:26 +00:00
JavaScript port decodes YAPF 2; VS Code and Figma open every file
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
This commit is contained in:
parent
fdbd0a6b8f
commit
0ba1f0af17
23 changed files with 1751 additions and 66 deletions
|
|
@ -5,9 +5,13 @@ Click any `.yapf` file in VS Code to see the image.
|
|||
- Checkerboard behind transparent pixels.
|
||||
- **Click** to switch between *fit to window* and *100 %*; **Ctrl + scroll**
|
||||
to zoom (pixels stay sharp when enlarged).
|
||||
- Picker for every **mip level** stored in the file.
|
||||
- Info bar: size, channels, sRGB / linear, GPU format hint, file size and
|
||||
compression ratio, decode time.
|
||||
- Opens every YAPF file, version 1 and 2: GPU blocks (BC1–BC7, BC6H)
|
||||
decoded, 16-bit and float textures shown in 8-bit (floats sRGB encoded).
|
||||
- Pickers for **mip level**, cube **face**, array **layer**, animation
|
||||
**frame** and volume **slice**.
|
||||
- Info bar: size, shape, storage (e.g. *GPU BC7*, *RGB half float*), sRGB /
|
||||
linear, normal-map convention, format version, file size, decode time.
|
||||
Damaged files say so (checksum mismatch).
|
||||
- Reloads automatically when the file changes on disk.
|
||||
|
||||
Decoding runs in plain JavaScript inside the preview; no native code.
|
||||
|
|
@ -30,5 +34,6 @@ Should work in VS Code 1.75+ and VS Code–based editors such as VSCodium.
|
|||
|
||||
The `.vsix` packages cleanly, and the preview code was tested in a browser
|
||||
engine (headless Chromium) with real files: correct pixels, mip picker,
|
||||
error message for corrupt files. **It has not yet been run inside VS Code
|
||||
itself.**
|
||||
error message for corrupt files; the YAPF 2 preview was tested the same
|
||||
way (BC7, BC5, BC6H, half float, 16-bit, cube, animation, damaged and
|
||||
non-YAPF files). **It has not yet been run inside VS Code itself.**
|
||||
|
|
|
|||
|
|
@ -1,26 +1,49 @@
|
|||
// Webview side of the YAPF preview: decode, draw, zoom, mip picker, info.
|
||||
// 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 GPU_NAMES = ['RGBA8', 'RGB8', 'RG8', 'R8', 'SRGB8_A8', 'SRGB8'];
|
||||
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 image = null, level = 0, fit = true, zoom = 1;
|
||||
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 w = Math.max(1, image.width >>> level), h = Math.max(1, image.height >>> level);
|
||||
canvas.width = w; canvas.height = h;
|
||||
canvas.getContext('2d').putImageData(new ImageData(YAPF.toRGBA(image, level), w, h), 0, 0);
|
||||
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 (!image) return;
|
||||
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;
|
||||
|
|
@ -31,49 +54,76 @@
|
|||
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 {
|
||||
image = YAPF.decode(bytes);
|
||||
tex = YAPF.decodeTexture(bytes);
|
||||
} catch (e) {
|
||||
image = null;
|
||||
tex = null;
|
||||
errorBox.hidden = false;
|
||||
errorBox.textContent = name + ': ' + e.message;
|
||||
errorBox.textContent = name + ': ' + e.message.replace(/^YAPF: /, '');
|
||||
info.textContent = '';
|
||||
controls.textContent = '';
|
||||
return;
|
||||
}
|
||||
const ms = performance.now() - t;
|
||||
const raw = image.width * image.height * image.channels;
|
||||
level = Math.min(level, image.mipLevels - 1);
|
||||
info.textContent = [
|
||||
image.width + ' × ' + image.height,
|
||||
CH_NAMES[image.channels],
|
||||
(image.flags & 2 ? 'sRGB' : 'linear') + (image.flags & 1 ? ', premultiplied' : ''),
|
||||
'GPU ' + (GPU_NAMES[image.gpuFormat] || '?'),
|
||||
kb(bytes.length) + ' (' + (100 * bytes.length / raw).toFixed(1) + '% of raw)',
|
||||
'decoded in ' + ms.toFixed(1) + ' ms',
|
||||
].join(' · ');
|
||||
|
||||
controls.textContent = '';
|
||||
if (image.mipLevels > 1) {
|
||||
const sel = document.createElement('select');
|
||||
for (let m = 0; m < image.mipLevels; m++) {
|
||||
const o = document.createElement('option');
|
||||
o.value = m;
|
||||
o.textContent = 'Mip ' + m + ' (' + Math.max(1, image.width >>> m) + '×' + Math.max(1, image.height >>> m) + ')';
|
||||
sel.appendChild(o);
|
||||
}
|
||||
sel.value = level;
|
||||
sel.onchange = () => { level = +sel.value; draw(); };
|
||||
controls.appendChild(sel);
|
||||
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 || !image) return;
|
||||
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)));
|
||||
|
|
|
|||
Loading…
Reference in a new issue