quake2ts 0.0.634 → 0.0.635

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",
3
- "version": "0.0.634",
3
+ "version": "0.0.635",
4
4
  "description": "Quake II re-release port to TypeScript with WebGL renderer - A complete game engine with physics, networking, and BSP rendering",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -50,10 +50,12 @@ __export(index_exports, {
50
50
  MockWebGL2RenderingContext: () => MockWebGL2RenderingContext,
51
51
  captureAudioEvents: () => captureAudioEvents,
52
52
  captureCanvasDrawCalls: () => captureCanvasDrawCalls,
53
+ captureFramebufferAsPNG: () => captureFramebufferAsPNG,
53
54
  captureGameScreenshot: () => captureGameScreenshot,
54
55
  captureGameState: () => captureGameState,
55
56
  compareScreenshot: () => compareScreenshot,
56
57
  compareScreenshots: () => compareScreenshots,
58
+ compareSnapshots: () => compareSnapshots,
57
59
  createBandwidthTestScenario: () => createBandwidthTestScenario,
58
60
  createBinaryStreamMock: () => createBinaryStreamMock,
59
61
  createBinaryWriterMock: () => createBinaryWriterMock,
@@ -199,9 +201,12 @@ __export(index_exports, {
199
201
  createVector3: () => createVector3,
200
202
  createViewTestScenario: () => createViewTestScenario,
201
203
  createVisualTestScenario: () => createVisualTestScenario,
204
+ expectSnapshot: () => expectSnapshot,
205
+ getSnapshotPath: () => getSnapshotPath,
202
206
  initHeadlessWebGPU: () => initHeadlessWebGPU,
203
207
  intersects: () => import_shared2.intersects,
204
208
  ladderTrace: () => import_shared2.ladderTrace,
209
+ loadPNG: () => loadPNG,
205
210
  makeAxisBrush: () => makeAxisBrush,
206
211
  makeBrushFromMinsMaxs: () => makeBrushFromMinsMaxs,
207
212
  makeBspModel: () => makeBspModel,
@@ -213,7 +218,9 @@ __export(index_exports, {
213
218
  mockMonsterAttacks: () => mockMonsterAttacks,
214
219
  randomVector3: () => randomVector3,
215
220
  renderAndCapture: () => renderAndCapture,
221
+ renderAndExpectSnapshot: () => renderAndExpectSnapshot,
216
222
  runComputeAndReadback: () => runComputeAndReadback,
223
+ savePNG: () => savePNG,
217
224
  serializeUserInfo: () => serializeUserInfo,
218
225
  setupBrowserEnvironment: () => setupBrowserEnvironment,
219
226
  setupMockAudioContext: () => setupMockAudioContext,
@@ -3259,7 +3266,7 @@ function createMockAssetManager(overrides) {
3259
3266
  getMemoryUsage: import_vitest14.vi.fn().mockReturnValue({ textures: 0, audio: 0 }),
3260
3267
  clearCache: import_vitest14.vi.fn(),
3261
3268
  preloadAssets: import_vitest14.vi.fn().mockResolvedValue(void 0),
3262
- queueLoad: import_vitest14.vi.fn().mockImplementation((path2) => Promise.resolve({})),
3269
+ queueLoad: import_vitest14.vi.fn().mockImplementation((path3) => Promise.resolve({})),
3263
3270
  ...overrides
3264
3271
  };
3265
3272
  }
@@ -4643,6 +4650,185 @@ var verifySmoothing = (states) => {
4643
4650
  };
4644
4651
  };
4645
4652
 
4653
+ // src/visual/snapshots.ts
4654
+ var import_pngjs = require("pngjs");
4655
+ var import_pixelmatch = __toESM(require("pixelmatch"), 1);
4656
+ var import_promises = __toESM(require("fs/promises"), 1);
4657
+ var import_fs = require("fs");
4658
+ var import_path = __toESM(require("path"), 1);
4659
+ function getBytesPerRow(width) {
4660
+ const bytesPerPixel = 4;
4661
+ const unpaddedBytesPerRow = width * bytesPerPixel;
4662
+ const align = 256;
4663
+ const paddedBytesPerRow = Math.max(
4664
+ unpaddedBytesPerRow,
4665
+ Math.ceil(unpaddedBytesPerRow / align) * align
4666
+ );
4667
+ return paddedBytesPerRow;
4668
+ }
4669
+ async function captureFramebufferAsPNG(device, texture, options) {
4670
+ const { width, height, format = "rgba8unorm" } = options;
4671
+ const bytesPerRow = getBytesPerRow(width);
4672
+ const bufferSize = bytesPerRow * height;
4673
+ const outputBuffer = device.createBuffer({
4674
+ size: bufferSize,
4675
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
4676
+ label: "captureFramebufferAsPNG output buffer"
4677
+ });
4678
+ const commandEncoder = device.createCommandEncoder({ label: "captureFramebufferAsPNG encoder" });
4679
+ commandEncoder.copyTextureToBuffer(
4680
+ { texture },
4681
+ { buffer: outputBuffer, bytesPerRow, rowsPerImage: height },
4682
+ { width, height, depthOrArrayLayers: 1 }
4683
+ );
4684
+ device.queue.submit([commandEncoder.finish()]);
4685
+ await outputBuffer.mapAsync(GPUMapMode.READ);
4686
+ const mappedRange = outputBuffer.getMappedRange();
4687
+ const rawData = new Uint8Array(mappedRange);
4688
+ const bytesPerPixel = 4;
4689
+ const tightData = new Uint8ClampedArray(width * height * bytesPerPixel);
4690
+ for (let y = 0; y < height; y++) {
4691
+ const srcOffset = y * bytesPerRow;
4692
+ const dstOffset = y * width * bytesPerPixel;
4693
+ const rowSize = width * bytesPerPixel;
4694
+ for (let i = 0; i < rowSize; i++) {
4695
+ tightData[dstOffset + i] = rawData[srcOffset + i];
4696
+ }
4697
+ }
4698
+ outputBuffer.unmap();
4699
+ if (format === "bgra8unorm" || format === "bgra8unorm-srgb") {
4700
+ for (let i = 0; i < tightData.length; i += 4) {
4701
+ const b = tightData[i];
4702
+ const r = tightData[i + 2];
4703
+ tightData[i] = r;
4704
+ tightData[i + 2] = b;
4705
+ }
4706
+ }
4707
+ const png = new import_pngjs.PNG({ width, height });
4708
+ png.data = Buffer.from(tightData);
4709
+ return import_pngjs.PNG.sync.write(png);
4710
+ }
4711
+ async function savePNG(pixels, width, height, filepath) {
4712
+ const png = new import_pngjs.PNG({ width, height });
4713
+ png.data = Buffer.from(pixels);
4714
+ await import_promises.default.mkdir(import_path.default.dirname(filepath), { recursive: true });
4715
+ return new Promise((resolve, reject) => {
4716
+ const stream = (0, import_fs.createWriteStream)(filepath);
4717
+ stream.on("error", reject);
4718
+ stream.on("finish", resolve);
4719
+ png.pack().pipe(stream);
4720
+ });
4721
+ }
4722
+ async function loadPNG(filepath) {
4723
+ return new Promise((resolve, reject) => {
4724
+ const stream = (0, import_fs.createReadStream)(filepath);
4725
+ stream.on("error", reject);
4726
+ const png = new import_pngjs.PNG();
4727
+ png.on("error", reject);
4728
+ png.on("parsed", (data) => {
4729
+ resolve({
4730
+ data: new Uint8ClampedArray(data),
4731
+ width: png.width,
4732
+ height: png.height
4733
+ });
4734
+ });
4735
+ stream.pipe(png);
4736
+ });
4737
+ }
4738
+ async function compareSnapshots(actual, expected, width, height, options) {
4739
+ const {
4740
+ threshold = 0.1,
4741
+ includeAA = false,
4742
+ diffColor = [255, 0, 0],
4743
+ maxDifferencePercent = 0.1
4744
+ } = options || {};
4745
+ if (actual.length !== expected.length) {
4746
+ throw new Error(`Size mismatch: actual length ${actual.length} vs expected length ${expected.length}`);
4747
+ }
4748
+ const diff = new Uint8ClampedArray(width * height * 4);
4749
+ const numDiffPixels = (0, import_pixelmatch.default)(
4750
+ actual,
4751
+ expected,
4752
+ diff,
4753
+ width,
4754
+ height,
4755
+ {
4756
+ threshold,
4757
+ includeAA,
4758
+ diffColor,
4759
+ alpha: 1
4760
+ // Default alpha
4761
+ }
4762
+ );
4763
+ const totalPixels = width * height;
4764
+ const percentDifferent = numDiffPixels / totalPixels * 100;
4765
+ const passed = percentDifferent <= (maxDifferencePercent || 0);
4766
+ return {
4767
+ pixelsDifferent: numDiffPixels,
4768
+ totalPixels,
4769
+ percentDifferent,
4770
+ passed,
4771
+ diffImage: diff
4772
+ };
4773
+ }
4774
+ function getSnapshotPath(name, type, snapshotDir = "__snapshots__") {
4775
+ const dirMap = {
4776
+ baseline: "baselines",
4777
+ actual: "actual",
4778
+ diff: "diff"
4779
+ };
4780
+ return import_path.default.join(snapshotDir, dirMap[type], `${name}.png`);
4781
+ }
4782
+ async function expectSnapshot(pixels, options) {
4783
+ const {
4784
+ name,
4785
+ width,
4786
+ height,
4787
+ updateBaseline = false,
4788
+ snapshotDir = import_path.default.join(process.cwd(), "tests", "__snapshots__")
4789
+ // Default to current working dir/tests/__snapshots__
4790
+ } = options;
4791
+ if (!width || !height) {
4792
+ throw new Error("Width and height are required for expectSnapshot");
4793
+ }
4794
+ const baselinePath = getSnapshotPath(name, "baseline", snapshotDir);
4795
+ const actualPath = getSnapshotPath(name, "actual", snapshotDir);
4796
+ const diffPath = getSnapshotPath(name, "diff", snapshotDir);
4797
+ if (updateBaseline || !(0, import_fs.existsSync)(baselinePath)) {
4798
+ console.log(`Creating/Updating baseline for ${name} at ${baselinePath}`);
4799
+ await savePNG(pixels, width, height, baselinePath);
4800
+ return;
4801
+ }
4802
+ let baseline;
4803
+ try {
4804
+ baseline = await loadPNG(baselinePath);
4805
+ } catch (e) {
4806
+ throw new Error(`Failed to load baseline for ${name} at ${baselinePath}: ${e}`);
4807
+ }
4808
+ if (baseline.width !== width || baseline.height !== height) {
4809
+ await savePNG(pixels, width, height, actualPath);
4810
+ throw new Error(`Snapshot dimension mismatch for ${name}: expected ${baseline.width}x${baseline.height}, got ${width}x${height}`);
4811
+ }
4812
+ const result = await compareSnapshots(pixels, baseline.data, width, height, options);
4813
+ if (!result.passed) {
4814
+ await savePNG(pixels, width, height, actualPath);
4815
+ if (result.diffImage) {
4816
+ await savePNG(result.diffImage, width, height, diffPath);
4817
+ }
4818
+ throw new Error(
4819
+ `Snapshot comparison failed for ${name}: ${result.percentDifferent.toFixed(2)}% different (${result.pixelsDifferent} pixels). See ${diffPath} for details.`
4820
+ );
4821
+ }
4822
+ }
4823
+ async function renderAndExpectSnapshot(setup, renderFn, options) {
4824
+ const pixels = await renderAndCapture(setup, renderFn);
4825
+ await expectSnapshot(pixels, {
4826
+ ...options,
4827
+ width: setup.width,
4828
+ height: setup.height
4829
+ });
4830
+ }
4831
+
4646
4832
  // src/e2e/playwright.ts
4647
4833
  async function createPlaywrightTestClient(options = {}) {
4648
4834
  let playwright;
@@ -4725,8 +4911,8 @@ function throttleBandwidth(bytesPerSecond) {
4725
4911
 
4726
4912
  // src/e2e/visual.ts
4727
4913
  var import_canvas3 = require("@napi-rs/canvas");
4728
- var import_promises = __toESM(require("fs/promises"), 1);
4729
- var import_path = __toESM(require("path"), 1);
4914
+ var import_promises2 = __toESM(require("fs/promises"), 1);
4915
+ var import_path2 = __toESM(require("path"), 1);
4730
4916
  async function captureGameScreenshot(page, name) {
4731
4917
  return await page.screenshot({ path: `${name}.png` });
4732
4918
  }
@@ -4741,18 +4927,18 @@ async function takeScreenshot(canvas, filepath) {
4741
4927
  } else {
4742
4928
  throw new Error("Unsupported canvas type for screenshot");
4743
4929
  }
4744
- await import_promises.default.mkdir(import_path.default.dirname(filepath), { recursive: true });
4745
- await import_promises.default.writeFile(filepath, buffer);
4930
+ await import_promises2.default.mkdir(import_path2.default.dirname(filepath), { recursive: true });
4931
+ await import_promises2.default.writeFile(filepath, buffer);
4746
4932
  }
4747
4933
  async function compareScreenshot(canvas, baselinePath) {
4748
4934
  try {
4749
- await import_promises.default.access(baselinePath);
4935
+ await import_promises2.default.access(baselinePath);
4750
4936
  } catch {
4751
4937
  console.warn(`Baseline not found at ${baselinePath}, saving current as baseline.`);
4752
4938
  await takeScreenshot(canvas, baselinePath);
4753
4939
  return true;
4754
4940
  }
4755
- const baselineBuffer = await import_promises.default.readFile(baselinePath);
4941
+ const baselineBuffer = await import_promises2.default.readFile(baselinePath);
4756
4942
  const baselineImage = new import_canvas3.Image();
4757
4943
  baselineImage.src = baselineBuffer;
4758
4944
  const width = baselineImage.width;
@@ -4844,10 +5030,12 @@ function createVisualTestScenario(sceneName) {
4844
5030
  MockWebGL2RenderingContext,
4845
5031
  captureAudioEvents,
4846
5032
  captureCanvasDrawCalls,
5033
+ captureFramebufferAsPNG,
4847
5034
  captureGameScreenshot,
4848
5035
  captureGameState,
4849
5036
  compareScreenshot,
4850
5037
  compareScreenshots,
5038
+ compareSnapshots,
4851
5039
  createBandwidthTestScenario,
4852
5040
  createBinaryStreamMock,
4853
5041
  createBinaryWriterMock,
@@ -4993,9 +5181,12 @@ function createVisualTestScenario(sceneName) {
4993
5181
  createVector3,
4994
5182
  createViewTestScenario,
4995
5183
  createVisualTestScenario,
5184
+ expectSnapshot,
5185
+ getSnapshotPath,
4996
5186
  initHeadlessWebGPU,
4997
5187
  intersects,
4998
5188
  ladderTrace,
5189
+ loadPNG,
4999
5190
  makeAxisBrush,
5000
5191
  makeBrushFromMinsMaxs,
5001
5192
  makeBspModel,
@@ -5007,7 +5198,9 @@ function createVisualTestScenario(sceneName) {
5007
5198
  mockMonsterAttacks,
5008
5199
  randomVector3,
5009
5200
  renderAndCapture,
5201
+ renderAndExpectSnapshot,
5010
5202
  runComputeAndReadback,
5203
+ savePNG,
5011
5204
  serializeUserInfo,
5012
5205
  setupBrowserEnvironment,
5013
5206
  setupMockAudioContext,