@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.
@@ -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';
@@ -1,4 +1,4 @@
1
- import type { Vec3, Bounds3 } from '@quake2ts/shared/math/vec3';
1
+ import type { Vec3, Bounds3 } from '@quake2ts/shared';
2
2
 
3
3
  /**
4
4
  * Creates a Vector3 object.