@quake2ts/test-utils 0.0.838 → 0.0.839
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +622 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -11
- package/dist/index.d.ts +137 -11
- package/dist/index.js +606 -0
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/engine/builders/bspBuilder.ts +278 -0
- package/src/engine/builders/md2Builder.ts +156 -0
- package/src/engine/builders/md3Builder.ts +149 -0
- package/src/engine/builders/pakBuilder.ts +50 -0
- package/src/engine/builders/pcxBuilder.ts +51 -0
- package/src/engine/builders/visual-testing.ts +144 -0
- package/src/engine/builders/walBuilder.ts +40 -0
- package/src/engine/builders/wavBuilder.ts +49 -0
- package/src/index.ts +7 -0
- package/src/shared/math.ts +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
interface PcxOptions {
|
|
2
|
+
readonly width: number;
|
|
3
|
+
readonly height: number;
|
|
4
|
+
readonly pixels: readonly number[];
|
|
5
|
+
readonly palette?: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function buildPcx(options: PcxOptions): ArrayBuffer {
|
|
9
|
+
const { width, height } = options;
|
|
10
|
+
const headerSize = 128;
|
|
11
|
+
const paletteSize = 769;
|
|
12
|
+
const encodedPixels: number[] = [];
|
|
13
|
+
for (const value of options.pixels) {
|
|
14
|
+
if (value >= 0xc0) {
|
|
15
|
+
encodedPixels.push(0xc1, value);
|
|
16
|
+
} else {
|
|
17
|
+
encodedPixels.push(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const imageSize = encodedPixels.length;
|
|
21
|
+
const buffer = new ArrayBuffer(headerSize + imageSize + paletteSize);
|
|
22
|
+
const view = new DataView(buffer);
|
|
23
|
+
view.setUint8(0, 0x0a); // manufacturer
|
|
24
|
+
view.setUint8(1, 5); // version
|
|
25
|
+
view.setUint8(2, 1); // encoding
|
|
26
|
+
view.setUint8(3, 8); // bits per pixel
|
|
27
|
+
view.setUint16(4, 0, true);
|
|
28
|
+
view.setUint16(6, 0, true);
|
|
29
|
+
view.setUint16(8, width - 1, true);
|
|
30
|
+
view.setUint16(10, height - 1, true);
|
|
31
|
+
view.setUint16(66, width, true);
|
|
32
|
+
|
|
33
|
+
const encoded = new Uint8Array(buffer, headerSize, imageSize);
|
|
34
|
+
encoded.set(encodedPixels);
|
|
35
|
+
|
|
36
|
+
const paletteMarkerOffset = headerSize + imageSize;
|
|
37
|
+
view.setUint8(paletteMarkerOffset, 0x0c);
|
|
38
|
+
const palette = new Uint8Array(buffer, paletteMarkerOffset + 1, 768);
|
|
39
|
+
palette.fill(0);
|
|
40
|
+
if (options.palette) {
|
|
41
|
+
palette.set(options.palette.subarray(0, 768));
|
|
42
|
+
} else {
|
|
43
|
+
for (let i = 0; i < 256; i += 1) {
|
|
44
|
+
palette[i * 3] = i;
|
|
45
|
+
palette[i * 3 + 1] = 255 - i;
|
|
46
|
+
palette[i * 3 + 2] = i;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return buffer;
|
|
51
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { test as base } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
expectSnapshot,
|
|
4
|
+
createRenderTestSetup,
|
|
5
|
+
RenderTestSetup,
|
|
6
|
+
captureTexture
|
|
7
|
+
} from '@quake2ts/test-utils';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
interface VisualTestContext {
|
|
11
|
+
expectSnapshot: (pixels: Uint8ClampedArray, nameOrOptions: string | { name: string; description: string }) => Promise<void>;
|
|
12
|
+
renderAndExpectSnapshot: (
|
|
13
|
+
fn: (
|
|
14
|
+
device: GPUDevice,
|
|
15
|
+
format: GPUTextureFormat,
|
|
16
|
+
encoder: GPUCommandEncoder,
|
|
17
|
+
view: GPUTextureView
|
|
18
|
+
) => Promise<((pass: GPURenderPassEncoder) => void) | void>,
|
|
19
|
+
nameOrOptions: string | { name: string; description: string; depth?: boolean }
|
|
20
|
+
) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const test = base.extend<VisualTestContext>({
|
|
24
|
+
expectSnapshot: async ({ task }, use) => {
|
|
25
|
+
const impl = async (pixels: Uint8ClampedArray, nameOrOptions: string | { name: string; description: string }) => {
|
|
26
|
+
const updateBaseline = process.env.UPDATE_VISUAL === '1' || process.argv.includes('--update-snapshots') || process.argv.includes('-u');
|
|
27
|
+
const testFile = task.file?.filepath;
|
|
28
|
+
const testDir = testFile ? path.dirname(testFile) : path.join(process.cwd(), 'tests');
|
|
29
|
+
const snapshotDir = path.join(testDir, '__snapshots__');
|
|
30
|
+
|
|
31
|
+
const name = typeof nameOrOptions === 'string' ? nameOrOptions : nameOrOptions.name;
|
|
32
|
+
const description = typeof nameOrOptions === 'string' ? undefined : nameOrOptions.description;
|
|
33
|
+
|
|
34
|
+
await expectSnapshot(pixels, {
|
|
35
|
+
name,
|
|
36
|
+
description,
|
|
37
|
+
width: 256,
|
|
38
|
+
height: 256,
|
|
39
|
+
updateBaseline,
|
|
40
|
+
snapshotDir
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
await use(impl);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
renderAndExpectSnapshot: async ({ task }, use) => {
|
|
47
|
+
const updateBaseline = process.env.UPDATE_VISUAL === '1' || process.argv.includes('--update-snapshots') || process.argv.includes('-u');
|
|
48
|
+
const testFile = task.file?.filepath;
|
|
49
|
+
const testDir = testFile ? path.dirname(testFile) : path.join(process.cwd(), 'tests');
|
|
50
|
+
const snapshotDir = path.join(testDir, '__snapshots__');
|
|
51
|
+
|
|
52
|
+
let setup: RenderTestSetup | undefined;
|
|
53
|
+
|
|
54
|
+
const impl = async (
|
|
55
|
+
fn: (
|
|
56
|
+
device: GPUDevice,
|
|
57
|
+
format: GPUTextureFormat,
|
|
58
|
+
encoder: GPUCommandEncoder,
|
|
59
|
+
view: GPUTextureView
|
|
60
|
+
) => Promise<((pass: GPURenderPassEncoder) => void) | void>,
|
|
61
|
+
nameOrOptions: string | { name: string; description: string; depth?: boolean }
|
|
62
|
+
) => {
|
|
63
|
+
const name = typeof nameOrOptions === 'string' ? nameOrOptions : nameOrOptions.name;
|
|
64
|
+
const description = typeof nameOrOptions === 'string' ? undefined : nameOrOptions.description;
|
|
65
|
+
const depth = typeof nameOrOptions === 'string' ? false : !!nameOrOptions.depth;
|
|
66
|
+
|
|
67
|
+
setup = await createRenderTestSetup(256, 256, { depth });
|
|
68
|
+
const { device } = setup.context;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const commandEncoder = device.createCommandEncoder();
|
|
72
|
+
|
|
73
|
+
// Allow the test to create resources and get the render function
|
|
74
|
+
// We pass all context info so the test can manage passes manually if needed
|
|
75
|
+
// Fallback to 'rgba8unorm' if format is missing (quick fix)
|
|
76
|
+
const renderFn = await fn(
|
|
77
|
+
device,
|
|
78
|
+
setup.context.format || 'rgba8unorm',
|
|
79
|
+
commandEncoder,
|
|
80
|
+
setup.renderTargetView
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
device.pushErrorScope('validation');
|
|
84
|
+
|
|
85
|
+
if (typeof renderFn === 'function') {
|
|
86
|
+
// Legacy mode: Wrap in a render pass
|
|
87
|
+
const passDescriptor: GPURenderPassDescriptor = {
|
|
88
|
+
colorAttachments: [{
|
|
89
|
+
view: setup.renderTargetView,
|
|
90
|
+
loadOp: 'clear',
|
|
91
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
92
|
+
storeOp: 'store'
|
|
93
|
+
}]
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (setup.depthTargetView) {
|
|
97
|
+
passDescriptor.depthStencilAttachment = {
|
|
98
|
+
view: setup.depthTargetView,
|
|
99
|
+
depthClearValue: 1.0,
|
|
100
|
+
depthLoadOp: 'clear',
|
|
101
|
+
depthStoreOp: 'discard'
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pass = commandEncoder.beginRenderPass(passDescriptor);
|
|
106
|
+
renderFn(pass);
|
|
107
|
+
pass.end();
|
|
108
|
+
} else {
|
|
109
|
+
// Manual mode: User should have used encoder to record commands
|
|
110
|
+
// We ensure it's submitted
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
114
|
+
|
|
115
|
+
const error = await device.popErrorScope();
|
|
116
|
+
if (error) {
|
|
117
|
+
throw new Error(`WebGPU validation error: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const pixels = await captureTexture(device, setup.renderTarget, setup.width, setup.height);
|
|
121
|
+
|
|
122
|
+
await expectSnapshot(pixels, {
|
|
123
|
+
name,
|
|
124
|
+
description,
|
|
125
|
+
width: setup.width,
|
|
126
|
+
height: setup.height,
|
|
127
|
+
updateBaseline,
|
|
128
|
+
snapshotDir
|
|
129
|
+
});
|
|
130
|
+
} finally {
|
|
131
|
+
if (setup) {
|
|
132
|
+
await setup.cleanup();
|
|
133
|
+
setup = undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
await use(impl);
|
|
139
|
+
|
|
140
|
+
if (setup) {
|
|
141
|
+
await setup.cleanup();
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
interface WalOptions {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly width: number;
|
|
4
|
+
readonly height: number;
|
|
5
|
+
readonly palette?: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function buildWal(options: WalOptions): ArrayBuffer {
|
|
9
|
+
const { width, height } = options;
|
|
10
|
+
const headerSize = 100;
|
|
11
|
+
const mipSizes = [
|
|
12
|
+
width * height,
|
|
13
|
+
Math.max(1, (width >> 1) * (height >> 1)),
|
|
14
|
+
Math.max(1, (width >> 2) * (height >> 2)),
|
|
15
|
+
Math.max(1, (width >> 3) * (height >> 3)),
|
|
16
|
+
];
|
|
17
|
+
const totalSize = headerSize + mipSizes.reduce((a, b) => a + b, 0);
|
|
18
|
+
const buffer = new ArrayBuffer(totalSize);
|
|
19
|
+
const view = new DataView(buffer);
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
new Uint8Array(buffer, 0, 32).set(encoder.encode(options.name));
|
|
22
|
+
view.setInt32(32, width, true);
|
|
23
|
+
view.setInt32(36, height, true);
|
|
24
|
+
|
|
25
|
+
let offset = headerSize;
|
|
26
|
+
mipSizes.forEach((size, index) => {
|
|
27
|
+
view.setInt32(40 + index * 4, offset, true);
|
|
28
|
+
const data = new Uint8Array(buffer, offset, size);
|
|
29
|
+
for (let i = 0; i < size; i += 1) {
|
|
30
|
+
data[i] = (i + index) % 256;
|
|
31
|
+
}
|
|
32
|
+
offset += size;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
new Uint8Array(buffer, 56, 32).set(encoder.encode(options.name + '_anim'));
|
|
36
|
+
view.setInt32(88, 0, true);
|
|
37
|
+
view.setInt32(92, 0, true);
|
|
38
|
+
view.setInt32(96, 0, true);
|
|
39
|
+
return buffer;
|
|
40
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
interface WavOptions {
|
|
2
|
+
readonly sampleRate: number;
|
|
3
|
+
readonly channels: number;
|
|
4
|
+
readonly samples: readonly number[];
|
|
5
|
+
readonly bitsPerSample?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function buildWav(options: WavOptions): ArrayBuffer {
|
|
9
|
+
const bitsPerSample = options.bitsPerSample ?? 16;
|
|
10
|
+
const bytesPerSample = bitsPerSample / 8;
|
|
11
|
+
const frameCount = options.samples.length / options.channels;
|
|
12
|
+
const dataSize = frameCount * options.channels * bytesPerSample;
|
|
13
|
+
const buffer = new ArrayBuffer(44 + dataSize);
|
|
14
|
+
const view = new DataView(buffer);
|
|
15
|
+
|
|
16
|
+
const writeString = (offset: number, text: string) => {
|
|
17
|
+
new Uint8Array(buffer, offset, text.length).set(new TextEncoder().encode(text));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
writeString(0, 'RIFF');
|
|
21
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
22
|
+
writeString(8, 'WAVE');
|
|
23
|
+
writeString(12, 'fmt ');
|
|
24
|
+
view.setUint32(16, 16, true);
|
|
25
|
+
view.setUint16(20, 1, true);
|
|
26
|
+
view.setUint16(22, options.channels, true);
|
|
27
|
+
view.setUint32(24, options.sampleRate, true);
|
|
28
|
+
view.setUint32(28, options.sampleRate * options.channels * bytesPerSample, true);
|
|
29
|
+
view.setUint16(32, options.channels * bytesPerSample, true);
|
|
30
|
+
view.setUint16(34, bitsPerSample, true);
|
|
31
|
+
writeString(36, 'data');
|
|
32
|
+
view.setUint32(40, dataSize, true);
|
|
33
|
+
|
|
34
|
+
let offset = 44;
|
|
35
|
+
for (let i = 0; i < options.samples.length; i += 1) {
|
|
36
|
+
const sample = options.samples[i]!;
|
|
37
|
+
if (bitsPerSample === 8) {
|
|
38
|
+
view.setUint8(offset, Math.max(0, Math.min(255, Math.round(sample * 128 + 128))));
|
|
39
|
+
offset += 1;
|
|
40
|
+
} else if (bitsPerSample === 16) {
|
|
41
|
+
view.setInt16(offset, Math.round(sample * 32767), true);
|
|
42
|
+
offset += 2;
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error('Unsupported bit depth for builder');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return buffer;
|
|
49
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,13 @@ export * from './engine/helpers/webgl-playwright.js';
|
|
|
51
51
|
export * from './engine/helpers/pipeline-test-template.js';
|
|
52
52
|
export * from './engine/helpers/textures.js';
|
|
53
53
|
export * from './engine/renderers.js';
|
|
54
|
+
export * from './engine/builders/bspBuilder.js';
|
|
55
|
+
export * from './engine/builders/md2Builder.js';
|
|
56
|
+
export * from './engine/builders/md3Builder.js';
|
|
57
|
+
export * from './engine/builders/pakBuilder.js';
|
|
58
|
+
export * from './engine/builders/pcxBuilder.js';
|
|
59
|
+
export * from './engine/builders/walBuilder.js';
|
|
60
|
+
export * from './engine/builders/wavBuilder.js';
|
|
54
61
|
|
|
55
62
|
// Client Mocks
|
|
56
63
|
export * from './client/mocks/input.js';
|
package/src/shared/math.ts
CHANGED