@quake2ts/test-utils 0.0.764 → 0.0.766

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quake2ts/test-utils",
3
- "version": "0.0.764",
3
+ "version": "0.0.766",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -20,6 +20,11 @@
20
20
  "types": "./src/setup/webgpu.ts",
21
21
  "import": "./src/setup/webgpu.ts",
22
22
  "require": "./src/setup/webgpu.ts"
23
+ },
24
+ "./src/setup/headless-webgl": {
25
+ "types": "./src/setup/headless-webgl.ts",
26
+ "import": "./src/setup/headless-webgl.ts",
27
+ "require": "./src/setup/headless-webgl.ts"
23
28
  }
24
29
  },
25
30
  "files": [
@@ -39,10 +44,10 @@
39
44
  "pngjs": "^7.0.0",
40
45
  "vitest": "^1.6.0",
41
46
  "webgpu": "^0.3.8",
42
- "@quake2ts/engine": "^0.0.764",
43
- "@quake2ts/server": "0.0.764",
44
- "@quake2ts/game": "0.0.764",
45
- "@quake2ts/shared": "0.0.764"
47
+ "@quake2ts/engine": "^0.0.766",
48
+ "@quake2ts/game": "0.0.766",
49
+ "@quake2ts/server": "0.0.766",
50
+ "@quake2ts/shared": "0.0.766"
46
51
  },
47
52
  "peerDependenciesMeta": {
48
53
  "@quake2ts/engine": {
@@ -93,10 +98,10 @@
93
98
  "typescript": "^5.4.5",
94
99
  "vitest": "^1.6.0",
95
100
  "webgpu": "^0.3.8",
96
- "@quake2ts/engine": "^0.0.764",
97
- "@quake2ts/game": "0.0.764",
98
- "@quake2ts/server": "0.0.764",
99
- "@quake2ts/shared": "0.0.764"
101
+ "@quake2ts/engine": "^0.0.766",
102
+ "@quake2ts/game": "0.0.766",
103
+ "@quake2ts/server": "0.0.766",
104
+ "@quake2ts/shared": "0.0.766"
100
105
  },
101
106
  "optionalDependencies": {
102
107
  "gl": "^8.1.6"
@@ -0,0 +1,47 @@
1
+ import { createHeadlessWebGL, captureWebGLFramebuffer, HeadlessWebGLContext } from '../../setup/headless-webgl.js';
2
+ // Removed invalid import
3
+
4
+ export interface WebGLRenderTestSetup {
5
+ gl: WebGL2RenderingContext;
6
+ width: number;
7
+ height: number;
8
+ cleanup: () => void;
9
+ }
10
+
11
+ /**
12
+ * Creates a setup for testing WebGL rendering.
13
+ * Initializes a headless WebGL context.
14
+ */
15
+ export async function createWebGLRenderTestSetup(
16
+ width: number = 256,
17
+ height: number = 256
18
+ ): Promise<WebGLRenderTestSetup> {
19
+ const context = createHeadlessWebGL({ width, height });
20
+
21
+ return {
22
+ gl: context.gl,
23
+ width: context.width,
24
+ height: context.height,
25
+ cleanup: context.cleanup
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Helper to render and capture the output as pixel data.
31
+ * Executes the render function, ensures completion, and captures the framebuffer.
32
+ */
33
+ export async function renderAndCaptureWebGL(
34
+ setup: WebGLRenderTestSetup,
35
+ renderFn: (gl: WebGL2RenderingContext) => void
36
+ ): Promise<Uint8ClampedArray> {
37
+ const { gl, width, height } = setup;
38
+
39
+ // Execute the user's render function
40
+ renderFn(gl);
41
+
42
+ // Ensure all commands are finished before reading pixels
43
+ gl.finish();
44
+
45
+ // Capture the framebuffer content
46
+ return captureWebGLFramebuffer(gl, width, height);
47
+ }
package/src/index.ts CHANGED
@@ -29,7 +29,8 @@ export * from './server/helpers/bandwidth.js';
29
29
  export * from './setup/browser.js';
30
30
  export * from './setup/canvas.js';
31
31
  export * from './setup/webgpu.js';
32
- export * from './setup/headless-webgl.js'; // Added
32
+ // Safe to export now as gl is lazy loaded
33
+ export * from './setup/headless-webgl.js';
33
34
  export * from './engine/mocks/webgpu.js';
34
35
  export * from './setup/timing.js';
35
36
  export * from './setup/node.js';
@@ -44,6 +45,7 @@ export * from './engine/rendering.js';
44
45
  export * from './setup/storage.js';
45
46
  export * from './setup/audio.js';
46
47
  export * from './engine/helpers/webgpu-rendering.js';
48
+ export * from './engine/helpers/webgl-rendering.js';
47
49
  export * from './engine/helpers/pipeline-test-template.js';
48
50
 
49
51
  // Client Mocks
@@ -72,8 +74,9 @@ export type { StorageScenario } from './setup/storage.js';
72
74
  export type { NetworkSimulator, NetworkCondition } from './e2e/network.js';
73
75
  export type { VisualScenario, VisualDiff } from './e2e/visual.js';
74
76
  export type { HeadlessWebGPUSetup, WebGPUContextState } from './setup/webgpu.js';
75
- export type { HeadlessWebGLContext, HeadlessWebGLOptions } from './setup/headless-webgl.js'; // Added
77
+ export type { HeadlessWebGLContext, HeadlessWebGLOptions } from './setup/headless-webgl.js';
76
78
  export type { RenderTestSetup, ComputeTestSetup } from './engine/helpers/webgpu-rendering.js';
79
+ export type { WebGLRenderTestSetup } from './engine/helpers/webgl-rendering.js';
77
80
  export type { GeometryBuffers } from './engine/helpers/pipeline-test-template.js';
78
81
 
79
82
  // Shared Types
@@ -1,3 +1,7 @@
1
+ // Remove top-level import to avoid runtime crash when gl is missing
2
+ // import createGL from 'gl';
3
+ import type { WebGLContextState } from '@quake2ts/engine';
4
+
1
5
  export interface HeadlessWebGLOptions {
2
6
  width?: number;
3
7
  height?: number;
@@ -14,99 +18,98 @@ export interface HeadlessWebGLContext {
14
18
 
15
19
  /**
16
20
  * Creates a headless WebGL2 context using the 'gl' package.
17
- * Note: 'gl' is lazy-loaded to avoid issues in environments where it's not supported/needed.
21
+ * This is used for running WebGL tests in a Node.js environment without a browser.
18
22
  */
19
- export async function createHeadlessWebGL(
23
+ export function createHeadlessWebGL(
20
24
  options: HeadlessWebGLOptions = {}
21
- ): Promise<HeadlessWebGLContext> {
25
+ ): HeadlessWebGLContext {
22
26
  const width = options.width ?? 256;
23
27
  const height = options.height ?? 256;
24
28
 
25
- // Dynamically import gl
26
- // @ts-ignore - gl package might not be typed correctly for dynamic import or TS config
27
- const { default: gl } = await import('gl');
29
+ let createGL;
30
+ try {
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ createGL = require('gl');
33
+ } catch (e) {
34
+ throw new Error('gl package not found or failed to load. Install it to run WebGL tests.');
35
+ }
28
36
 
29
- // The 'gl' function signature is gl(width, height, options)
30
- const context = gl(width, height, {
31
- antialias: options.antialias ?? false, // Default to false for determinism
32
- preserveDrawingBuffer: options.preserveDrawingBuffer ?? true, // Default to true for readback
37
+ // Create WebGL context using 'gl' package
38
+ const glContext = createGL(width, height, {
39
+ antialias: options.antialias ?? false, // Default to false for deterministic testing
40
+ preserveDrawingBuffer: options.preserveDrawingBuffer ?? true, // Needed for readback
33
41
  stencil: true,
34
- alpha: true,
35
42
  depth: true,
43
+ alpha: true
36
44
  });
37
45
 
38
- if (!context) {
46
+ if (!glContext) {
39
47
  throw new Error('Failed to create headless WebGL context');
40
48
  }
41
49
 
42
- // Cast to WebGL2RenderingContext
43
- const glContext = context as unknown as WebGL2RenderingContext;
50
+ // Cast to WebGL2RenderingContext as 'gl' returns a compatible interface
51
+ // but TypeScript types might not align perfectly without casting
52
+ const gl = glContext as unknown as WebGL2RenderingContext;
53
+
54
+ // Verify context creation
55
+ const version = gl.getParameter(gl.VERSION);
56
+ // console.log(`Created headless WebGL context: ${version}`);
57
+
58
+ // Create cleanup function
59
+ const cleanup = () => {
60
+ const ext = gl.getExtension('STACKGL_destroy_context');
61
+ if (ext) {
62
+ ext.destroy();
63
+ }
64
+ };
44
65
 
45
66
  return {
46
- gl: glContext,
67
+ gl,
47
68
  width,
48
69
  height,
49
- cleanup: () => {
50
- // gl package extension to destroy context
51
- const ext = glContext.getExtension('STACKGL_destroy_context');
52
- if (ext) {
53
- ext.destroy();
54
- }
55
- },
70
+ cleanup
56
71
  };
57
72
  }
58
73
 
59
74
  /**
60
- * Captures the current framebuffer content as a Uint8ClampedArray (RGBA).
61
- * Flips the pixels vertically to match standard image orientation (top-left origin).
75
+ * Captures the current framebuffer content as a pixel array.
76
+ * Performs a vertical flip to match standard image coordinates (top-left origin).
62
77
  */
63
78
  export function captureWebGLFramebuffer(
64
- glContext: WebGL2RenderingContext,
79
+ gl: WebGL2RenderingContext,
65
80
  width: number,
66
81
  height: number
67
82
  ): Uint8ClampedArray {
68
83
  const pixels = new Uint8ClampedArray(width * height * 4);
69
84
 
70
- // readPixels reads from bottom-left
71
- glContext.readPixels(
72
- 0,
73
- 0,
74
- width,
75
- height,
76
- glContext.RGBA,
77
- glContext.UNSIGNED_BYTE,
78
- pixels
79
- );
85
+ // Read pixels from framebuffer (bottom-left origin)
86
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
80
87
 
88
+ // Flip vertically to match image coordinates (top-left origin)
81
89
  return flipPixelsVertically(pixels, width, height);
82
90
  }
83
91
 
84
92
  /**
85
- * Flips pixel data vertically in-place.
93
+ * Flips a pixel array vertically in-place or returns a new array.
94
+ * WebGL reads pixels bottom-up, but images are typically stored top-down.
86
95
  */
87
96
  export function flipPixelsVertically(
88
97
  pixels: Uint8ClampedArray,
89
98
  width: number,
90
99
  height: number
91
100
  ): Uint8ClampedArray {
101
+ const flipped = new Uint8ClampedArray(pixels.length);
92
102
  const rowSize = width * 4;
93
- const halfHeight = Math.floor(height / 2);
94
- const tempRow = new Uint8Array(rowSize);
95
-
96
- // Swap rows
97
- for (let y = 0; y < halfHeight; y++) {
98
- const topOffset = y * rowSize;
99
- const bottomOffset = (height - 1 - y) * rowSize;
100
-
101
- // Copy top to temp
102
- tempRow.set(pixels.subarray(topOffset, topOffset + rowSize));
103
103
 
104
- // Copy bottom to top
105
- pixels.copyWithin(topOffset, bottomOffset, bottomOffset + rowSize);
104
+ for (let y = 0; y < height; y++) {
105
+ const srcRowStart = y * rowSize;
106
+ const destRowStart = (height - 1 - y) * rowSize;
106
107
 
107
- // Copy temp to bottom
108
- pixels.set(tempRow, bottomOffset);
108
+ // Copy row
109
+ for (let i = 0; i < rowSize; i++) {
110
+ flipped[destRowStart + i] = pixels[srcRowStart + i];
111
+ }
109
112
  }
110
113
 
111
- return pixels;
114
+ return flipped;
112
115
  }