@forgeax/engine-image 0.0.0-dev.8d955ade1c79
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/LICENSE +202 -0
- package/README.md +166 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/compression-mode.unit.test.d.ts +2 -0
- package/dist/__tests__/compression-mode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/encode-source-too-large.unit.test.d.ts +2 -0
- package/dist/__tests__/encode-source-too-large.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test-d.d.ts +2 -0
- package/dist/__tests__/errors.test-d.d.ts.map +1 -0
- package/dist/__tests__/image-importer-conversion-failure.unit.test.d.ts +2 -0
- package/dist/__tests__/image-importer-conversion-failure.unit.test.d.ts.map +1 -0
- package/dist/__tests__/image-importer-hdr-equirect.test.d.ts +2 -0
- package/dist/__tests__/image-importer-hdr-equirect.test.d.ts.map +1 -0
- package/dist/__tests__/image-importer-topology.unit.test.d.ts +2 -0
- package/dist/__tests__/image-importer-topology.unit.test.d.ts.map +1 -0
- package/dist/__tests__/image-importer.test.d.ts +2 -0
- package/dist/__tests__/image-importer.test.d.ts.map +1 -0
- package/dist/__tests__/image-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/image-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/image.unit.test.d.ts +2 -0
- package/dist/__tests__/image.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ktx2-basis-importer.unit.test.d.ts +2 -0
- package/dist/__tests__/ktx2-basis-importer.unit.test.d.ts.map +1 -0
- package/dist/__tests__/make-fixture.d.ts +12 -0
- package/dist/__tests__/make-fixture.d.ts.map +1 -0
- package/dist/__tests__/parse-image-downscale.test.d.ts +2 -0
- package/dist/__tests__/parse-image-downscale.test.d.ts.map +1 -0
- package/dist/__tests__/source-key.unit.test.d.ts +2 -0
- package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
- package/dist/decode-image-from-file.d.ts +40 -0
- package/dist/decode-image-from-file.d.ts.map +1 -0
- package/dist/decode-image-from-file.mjs +460 -0
- package/dist/decode-image-from-file.mjs.map +1 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/hdr-decoder.d.ts +28 -0
- package/dist/hdr-decoder.d.ts.map +1 -0
- package/dist/hdr-decoder.mjs +212 -0
- package/dist/hdr-decoder.mjs.map +1 -0
- package/dist/image-decoder-browser.d.ts +22 -0
- package/dist/image-decoder-browser.d.ts.map +1 -0
- package/dist/image-decoder-node.d.ts +50 -0
- package/dist/image-decoder-node.d.ts.map +1 -0
- package/dist/image-importer.d.ts +18 -0
- package/dist/image-importer.d.ts.map +1 -0
- package/dist/image-importer.mjs +1085 -0
- package/dist/image-importer.mjs.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +469 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ktx2-encode.d.ts +81 -0
- package/dist/ktx2-encode.d.ts.map +1 -0
- package/dist/ktx2-encode.mjs +98 -0
- package/dist/ktx2-encode.mjs.map +1 -0
- package/dist/parse-image.d.ts +47 -0
- package/dist/parse-image.d.ts.map +1 -0
- package/dist/parse-image.mjs +154 -0
- package/dist/parse-image.mjs.map +1 -0
- package/dist/reimport-reuse-meta.d.ts +81 -0
- package/dist/reimport-reuse-meta.d.ts.map +1 -0
- package/dist/resize-image.d.ts +7 -0
- package/dist/resize-image.d.ts.map +1 -0
- package/dist/result.d.ts +2 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/runtime/asset-decoders.d.ts +4 -0
- package/dist/runtime/asset-decoders.d.ts.map +1 -0
- package/dist/source-key.d.ts +21 -0
- package/dist/source-key.d.ts.map +1 -0
- package/dist/sub-asset-key.d.ts +50 -0
- package/dist/sub-asset-key.d.ts.map +1 -0
- package/dist/to-asset-pack.d.ts +61 -0
- package/dist/to-asset-pack.d.ts.map +1 -0
- package/package.json +105 -0
- package/src/__tests__/compression-mode.unit.test.ts +104 -0
- package/src/__tests__/encode-source-too-large.unit.test.ts +81 -0
- package/src/__tests__/errors.test-d.ts +148 -0
- package/src/__tests__/image-importer-conversion-failure.unit.test.ts +297 -0
- package/src/__tests__/image-importer-hdr-equirect.test.ts +218 -0
- package/src/__tests__/image-importer-topology.unit.test.ts +209 -0
- package/src/__tests__/image-importer.test.ts +12 -0
- package/src/__tests__/image-local-artifacts.test.ts +38 -0
- package/src/__tests__/image.unit.test.ts +1253 -0
- package/src/__tests__/ktx2-basis-importer.unit.test.ts +126 -0
- package/src/__tests__/make-fixture.ts +100 -0
- package/src/__tests__/parse-image-downscale.test.ts +26 -0
- package/src/__tests__/source-key.unit.test.ts +65 -0
- package/src/decode-image-from-file.ts +233 -0
- package/src/errors.ts +60 -0
- package/src/hdr-decoder.ts +255 -0
- package/src/image-decoder-browser.ts +88 -0
- package/src/image-decoder-node.ts +67 -0
- package/src/image-decoders.d.ts +23 -0
- package/src/image-importer.ts +858 -0
- package/src/index.ts +44 -0
- package/src/ktx2-encode.ts +217 -0
- package/src/parse-image.ts +176 -0
- package/src/reimport-reuse-meta.ts +160 -0
- package/src/resize-image.ts +39 -0
- package/src/result.ts +13 -0
- package/src/runtime/asset-decoders.ts +152 -0
- package/src/source-key.ts +41 -0
- package/src/sub-asset-key.ts +67 -0
- package/src/to-asset-pack.ts +95 -0
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeax/engine-image",
|
|
3
|
+
"version": "0.0.0-dev.8d955ade1c79",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"description": "Disk-to-memory image importer for forgeax-engine: parseImage / decodeImageFromFile / toAssetPack three pure functions + sub-asset-key + reimport-reuse-meta + correlated ImageError closed union.",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./parse-image": {
|
|
15
|
+
"types": "./dist/parse-image.d.ts",
|
|
16
|
+
"node": {
|
|
17
|
+
"import": "./dist/parse-image.mjs"
|
|
18
|
+
},
|
|
19
|
+
"default": null
|
|
20
|
+
},
|
|
21
|
+
"./decode-image-from-file": {
|
|
22
|
+
"types": "./dist/decode-image-from-file.d.ts",
|
|
23
|
+
"node": {
|
|
24
|
+
"import": "./dist/decode-image-from-file.mjs"
|
|
25
|
+
},
|
|
26
|
+
"default": null
|
|
27
|
+
},
|
|
28
|
+
"./hdr-decoder": {
|
|
29
|
+
"types": "./dist/hdr-decoder.d.ts",
|
|
30
|
+
"import": "./dist/hdr-decoder.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./image-importer": {
|
|
33
|
+
"types": "./dist/image-importer.d.ts",
|
|
34
|
+
"node": {
|
|
35
|
+
"import": "./dist/image-importer.mjs"
|
|
36
|
+
},
|
|
37
|
+
"default": null
|
|
38
|
+
},
|
|
39
|
+
"./ktx2-encode": {
|
|
40
|
+
"types": "./dist/ktx2-encode.d.ts",
|
|
41
|
+
"node": {
|
|
42
|
+
"import": "./dist/ktx2-encode.mjs"
|
|
43
|
+
},
|
|
44
|
+
"default": null
|
|
45
|
+
},
|
|
46
|
+
"./package.json": "./package.json"
|
|
47
|
+
},
|
|
48
|
+
"main": "./dist/index.mjs",
|
|
49
|
+
"types": "./dist/index.d.ts",
|
|
50
|
+
"files": [
|
|
51
|
+
"dist",
|
|
52
|
+
"src",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
],
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@forgeax/engine-codec": "0.0.0-dev.8d955ade1c79",
|
|
58
|
+
"@forgeax/engine-math": "0.0.0-dev.8d955ade1c79",
|
|
59
|
+
"@forgeax/engine-pack": "0.0.0-dev.8d955ade1c79",
|
|
60
|
+
"@forgeax/engine-types": "0.0.0-dev.8d955ade1c79",
|
|
61
|
+
"jpeg-js": "^0.4.4",
|
|
62
|
+
"pako": "^1.0.11",
|
|
63
|
+
"upng-js": "^2.1.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@forgeax/engine-import": "0.0.0-dev.8d955ade1c79",
|
|
67
|
+
"@types/node": "^20.14.0",
|
|
68
|
+
"@webgpu/types": "^0.1.71"
|
|
69
|
+
},
|
|
70
|
+
"forgeax": {
|
|
71
|
+
"metrics": {
|
|
72
|
+
"bundle-size": {
|
|
73
|
+
"enabled": true,
|
|
74
|
+
"path": "dist/index.mjs",
|
|
75
|
+
"compression": "gzip",
|
|
76
|
+
"expected": 2881,
|
|
77
|
+
"baseline": {
|
|
78
|
+
"source": ".forgeax-harness/forgeax-loop/feat-20260515-learn-render-getting-started/plan-strategy.md#5.4",
|
|
79
|
+
"threshold": 307200
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"fps": {
|
|
83
|
+
"enabled": false,
|
|
84
|
+
"reason": "library package, no runtime canvas; fps is reported by hello-* / learn-render apps that consume DecodedImage POD"
|
|
85
|
+
},
|
|
86
|
+
"bench": {
|
|
87
|
+
"enabled": false,
|
|
88
|
+
"reason": "image decode is one-shot importer-side work; bench focus stays on @forgeax/engine-math for perf-critical hot paths"
|
|
89
|
+
},
|
|
90
|
+
"gate": {
|
|
91
|
+
"enabled": false,
|
|
92
|
+
"reason": "no package-level binary gate; the disk-to-memory pipeline isolation is policed by node scripts/check-image-pipeline-isolation.mjs in M2 milestoneCISweep"
|
|
93
|
+
},
|
|
94
|
+
"spike-report": {
|
|
95
|
+
"enabled": false,
|
|
96
|
+
"reason": "not a spike package; this is a production MVP per plan-strategy D-4 / D-12 (image importer + ImageErrorCode independent closed union)"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"scripts": {
|
|
101
|
+
"build": "tsup",
|
|
102
|
+
"test": "vitest run",
|
|
103
|
+
"typecheck": "tsc -b"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { basisEncodeParamsFor, resolveEncodeMode } from '../ktx2-encode.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* compressionMode sidecar + 'auto' derivation unit tests (M3 w16).
|
|
6
|
+
*
|
|
7
|
+
* Tests the pure image-side derivation the offline encode arm relies on
|
|
8
|
+
* (D-12), independent of the WASM encoder (round-trip bytes are w15's job;
|
|
9
|
+
* import-texture wiring is w20's job; no browser DOM):
|
|
10
|
+
*
|
|
11
|
+
* - the four sidecar values ('auto' / 'etc1s' / 'uastc' / 'none') each
|
|
12
|
+
* resolve to a delivery encoding;
|
|
13
|
+
* - 'auto' derives deterministically from existing sidecar signals (D-12):
|
|
14
|
+
* colorSpace 'srgb' -> ETC1S (albedo / UI), colorSpace 'linear' -> UASTC
|
|
15
|
+
* (normals / ORM / data), HDR source -> UASTC-HDR;
|
|
16
|
+
* - the 'none' path resolves to 'none' and yields NO encoder params (the
|
|
17
|
+
* importer keeps the existing .bin path -- the encoder is not called);
|
|
18
|
+
* - the encoder param mapping (basis tex format / srgb transfer flag / mip
|
|
19
|
+
* gen) is correct per resolved mode.
|
|
20
|
+
*
|
|
21
|
+
* Note (M3 sequencing constraint): 'auto' derivation is implemented + tested
|
|
22
|
+
* here but is NOT the default value. The sidecar default stays 'none' until
|
|
23
|
+
* M5 (w38) flips it -- this milestone must not make existing textures encode
|
|
24
|
+
* to basis ahead of loader support (plan R-9).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
describe('resolveEncodeMode -- four sidecar values (M3 w16)', () => {
|
|
28
|
+
it("explicit 'etc1s' resolves to etc1s", () => {
|
|
29
|
+
expect(resolveEncodeMode('etc1s', { colorSpace: 'srgb', isHdr: false })).toBe('etc1s');
|
|
30
|
+
expect(resolveEncodeMode('etc1s', { colorSpace: 'linear', isHdr: false })).toBe('etc1s');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("explicit 'uastc' resolves to uastc (LDR) or uastc-hdr for an HDR source", () => {
|
|
34
|
+
expect(resolveEncodeMode('uastc', { colorSpace: 'linear', isHdr: false })).toBe('uastc');
|
|
35
|
+
expect(resolveEncodeMode('uastc', { colorSpace: 'srgb', isHdr: false })).toBe('uastc');
|
|
36
|
+
expect(resolveEncodeMode('uastc', { colorSpace: 'linear', isHdr: true })).toBe('uastc-hdr');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("explicit 'none' resolves to none", () => {
|
|
40
|
+
expect(resolveEncodeMode('none', { colorSpace: 'srgb', isHdr: false })).toBe('none');
|
|
41
|
+
expect(resolveEncodeMode('none', { colorSpace: 'linear', isHdr: true })).toBe('none');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("resolveEncodeMode -- 'auto' derivation (D-12, M3 w16)", () => {
|
|
46
|
+
it('srgb color source -> etc1s', () => {
|
|
47
|
+
expect(resolveEncodeMode('auto', { colorSpace: 'srgb', isHdr: false })).toBe('etc1s');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('linear (data) source -> uastc', () => {
|
|
51
|
+
expect(resolveEncodeMode('auto', { colorSpace: 'linear', isHdr: false })).toBe('uastc');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('HDR source -> uastc-hdr (regardless of colorSpace)', () => {
|
|
55
|
+
expect(resolveEncodeMode('auto', { colorSpace: 'linear', isHdr: true })).toBe('uastc-hdr');
|
|
56
|
+
expect(resolveEncodeMode('auto', { colorSpace: 'srgb', isHdr: true })).toBe('uastc-hdr');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('basisEncodeParamsFor -- mode -> encoder params (M3 w16)', () => {
|
|
61
|
+
it("'none' yields no encoder params (encoder is not called)", () => {
|
|
62
|
+
expect(basisEncodeParamsFor('none', { colorSpace: 'srgb', isHdr: false })).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('etc1s maps to the ETC1S tex format with perceptual metrics for srgb color', () => {
|
|
66
|
+
const p = basisEncodeParamsFor('etc1s', { colorSpace: 'srgb', isHdr: false });
|
|
67
|
+
expect(p).not.toBeNull();
|
|
68
|
+
if (p === null) return;
|
|
69
|
+
expect(p.mode).toBe('etc1s');
|
|
70
|
+
expect(p.srgb).toBe(true);
|
|
71
|
+
expect(p.perceptual).toBe(true);
|
|
72
|
+
expect(p.uastcSupercompression).toBe(false);
|
|
73
|
+
expect(p.mipGen).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('etc1s on a linear source drops the srgb transfer + perceptual flags', () => {
|
|
77
|
+
const p = basisEncodeParamsFor('etc1s', { colorSpace: 'linear', isHdr: false });
|
|
78
|
+
expect(p).not.toBeNull();
|
|
79
|
+
if (p === null) return;
|
|
80
|
+
expect(p.mode).toBe('etc1s');
|
|
81
|
+
expect(p.srgb).toBe(false);
|
|
82
|
+
expect(p.perceptual).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('uastc maps to the UASTC-LDR tex format with zstd supercompression', () => {
|
|
86
|
+
const p = basisEncodeParamsFor('uastc', { colorSpace: 'linear', isHdr: false });
|
|
87
|
+
expect(p).not.toBeNull();
|
|
88
|
+
if (p === null) return;
|
|
89
|
+
expect(p.mode).toBe('uastc-ldr');
|
|
90
|
+
expect(p.srgb).toBe(false);
|
|
91
|
+
expect(p.uastcSupercompression).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("'uastc' on an HDR source maps to the UASTC-HDR tex format with no srgb transfer", () => {
|
|
95
|
+
// 'uastc-hdr' is a derived delivery encoding, not a sidecar value; it is
|
|
96
|
+
// reached via the 'uastc' (or 'auto') sidecar mode on an HDR source.
|
|
97
|
+
const p = basisEncodeParamsFor('uastc', { colorSpace: 'linear', isHdr: true });
|
|
98
|
+
expect(p).not.toBeNull();
|
|
99
|
+
if (p === null) return;
|
|
100
|
+
expect(p.mode).toBe('uastc-hdr');
|
|
101
|
+
expect(p.srgb).toBe(false);
|
|
102
|
+
expect(p.uastcSupercompression).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
type EncodeSourceInfo,
|
|
4
|
+
encodeTextureToKtx2,
|
|
5
|
+
MAX_ENCODE_SOURCE_PIXELS,
|
|
6
|
+
} from '../ktx2-encode.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Encoder source-pixel fail-fast gate (CI-red fix, human decision).
|
|
10
|
+
*
|
|
11
|
+
* The wasm32 Basis encoder ceiling is 4096x4096 = 16.78 Mpx (MAX_ENCODE_SOURCE_PIXELS);
|
|
12
|
+
* above it the raw encoder returns 0 bytes silently. `encodeTextureToKtx2` pre-checks
|
|
13
|
+
* width*height and, when it exceeds the ceiling, returns a structured
|
|
14
|
+
* `ktx2-encode-source-too-large` error WITHOUT calling the encoder -- so the failure is
|
|
15
|
+
* actionable ("reduce resolution or set compressionMode:'none'") instead of an opaque
|
|
16
|
+
* 0-byte encode failure. This test asserts both the boundary behavior and that the
|
|
17
|
+
* encoder is never reached for an over-limit source.
|
|
18
|
+
*
|
|
19
|
+
* The over-limit path is exercised with a tiny buffer + explicit dimensions: the
|
|
20
|
+
* pre-check only reads width*height, never the pixel bytes, so no real megapixel
|
|
21
|
+
* allocation is needed and no WASM is loaded.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Spy on the codec encode binding: an over-limit source must never reach it.
|
|
25
|
+
const basisEncodeSpy = vi.fn();
|
|
26
|
+
vi.mock('@forgeax/engine-codec/encode', () => ({
|
|
27
|
+
basisEncode: (...args: unknown[]) => basisEncodeSpy(...args),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const SRGB: EncodeSourceInfo = { colorSpace: 'srgb', isHdr: false };
|
|
31
|
+
|
|
32
|
+
describe('encodeTextureToKtx2 -- source-pixel ceiling fail-fast', () => {
|
|
33
|
+
it('MAX_ENCODE_SOURCE_PIXELS is 4096x4096 (16.78 Mpx)', () => {
|
|
34
|
+
expect(MAX_ENCODE_SOURCE_PIXELS).toBe(4096 * 4096);
|
|
35
|
+
expect(MAX_ENCODE_SOURCE_PIXELS).toBe(16_777_216);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('rejects a >4096^2 source with ktx2-encode-source-too-large, without calling the encoder', async () => {
|
|
39
|
+
basisEncodeSpy.mockClear();
|
|
40
|
+
// 8192x8192 = 67 Mpx, well over the ceiling. A 4-byte stub buffer suffices:
|
|
41
|
+
// the pre-check reads only the dimensions.
|
|
42
|
+
const r = await encodeTextureToKtx2(new Uint8Array(4), 8192, 8192, 'etc1s', SRGB);
|
|
43
|
+
expect(r.ok).toBe(false);
|
|
44
|
+
if (r.ok) return;
|
|
45
|
+
expect(r.error.code).toBe('ktx2-encode-source-too-large');
|
|
46
|
+
expect(r.error.mode).toBe('etc1s');
|
|
47
|
+
expect(r.error.reason).toContain('8192x8192');
|
|
48
|
+
expect(r.error.reason).toContain('4096x4096');
|
|
49
|
+
expect(basisEncodeSpy).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects the first over-limit size just past the ceiling (4097x4096)', async () => {
|
|
53
|
+
basisEncodeSpy.mockClear();
|
|
54
|
+
const r = await encodeTextureToKtx2(new Uint8Array(4), 4097, 4096, 'etc1s', SRGB);
|
|
55
|
+
expect(r.ok).toBe(false);
|
|
56
|
+
if (r.ok) return;
|
|
57
|
+
expect(r.error.code).toBe('ktx2-encode-source-too-large');
|
|
58
|
+
expect(basisEncodeSpy).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('an exactly-4096^2 source passes the ceiling and reaches the encoder', async () => {
|
|
62
|
+
basisEncodeSpy.mockClear();
|
|
63
|
+
basisEncodeSpy.mockResolvedValue({ ok: true, value: new Uint8Array([1, 2, 3]) });
|
|
64
|
+
const r = await encodeTextureToKtx2(new Uint8Array(4), 4096, 4096, 'etc1s', SRGB);
|
|
65
|
+
expect(basisEncodeSpy).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(r.ok).toBe(true);
|
|
67
|
+
if (!r.ok) return;
|
|
68
|
+
expect(r.value.mode).toBe('etc1s');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("the 'none' path still short-circuits before the ceiling check", async () => {
|
|
72
|
+
basisEncodeSpy.mockClear();
|
|
73
|
+
// Over-limit dimensions but mode 'none' -> resolves to null params first.
|
|
74
|
+
const r = await encodeTextureToKtx2(new Uint8Array(4), 8192, 8192, 'none', SRGB);
|
|
75
|
+
expect(r.ok).toBe(false);
|
|
76
|
+
if (r.ok) return;
|
|
77
|
+
expect(r.error.code).toBe('ktx2-encode-failed');
|
|
78
|
+
expect(r.error.mode).toBe('none');
|
|
79
|
+
expect(basisEncodeSpy).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DecodedImage,
|
|
3
|
+
ImageError,
|
|
4
|
+
ImageErrorCode,
|
|
5
|
+
ImageErrorDetail,
|
|
6
|
+
ImageErrorDetailFor,
|
|
7
|
+
ImageErrorFor,
|
|
8
|
+
ImageMeta,
|
|
9
|
+
} from '@forgeax/engine-types';
|
|
10
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
11
|
+
|
|
12
|
+
// Type-only tests. Verify the 8-member ImageErrorCode closed union compiles
|
|
13
|
+
// an exhaustive switch without a default arm (charter P4 explicit failure).
|
|
14
|
+
// Verifies the discriminated ImageErrorDetail narrowing per .code (charter
|
|
15
|
+
// P3 machine-readable union > prose; AGENTS.md Error model row pattern).
|
|
16
|
+
//
|
|
17
|
+
// feat-20260521-sprite-atlas-animation M1 T-03 update: union grew from 5 to
|
|
18
|
+
// 8 with the atlas-* triplet (atlas-empty-input / atlas-size-exceeded /
|
|
19
|
+
// atlas-region-mismatch). The exact-member assertion + exhaustive switch
|
|
20
|
+
// arms below mirror the SSOT in @forgeax/engine-types ImageErrorCode +
|
|
21
|
+
// ImageErrorDetail so a future minor add still fails this file at compile
|
|
22
|
+
// time until the new arm + .expected + .hint rows are supplied.
|
|
23
|
+
|
|
24
|
+
describe('ImageErrorCode + ImageErrorDetail closed union compile contract', () => {
|
|
25
|
+
it('ImageErrorCode is the exact 8-member union literal set', () => {
|
|
26
|
+
expectTypeOf<ImageErrorCode>().toEqualTypeOf<
|
|
27
|
+
| 'image-decode-failed'
|
|
28
|
+
| 'image-format-unsupported'
|
|
29
|
+
| 'image-dimension-out-of-bounds'
|
|
30
|
+
| 'image-meta-missing'
|
|
31
|
+
| 'image-hdr-decode-failed'
|
|
32
|
+
| 'atlas-empty-input'
|
|
33
|
+
| 'atlas-size-exceeded'
|
|
34
|
+
| 'atlas-region-mismatch'
|
|
35
|
+
>();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('exhaustive switch on err.code compiles with no default arm (charter P4)', () => {
|
|
39
|
+
function recover(code: ImageErrorCode): string {
|
|
40
|
+
switch (code) {
|
|
41
|
+
case 'image-decode-failed':
|
|
42
|
+
return 'check file integrity / re-export from DCC tool';
|
|
43
|
+
case 'image-format-unsupported':
|
|
44
|
+
return 'convert to PNG or JPG';
|
|
45
|
+
case 'image-dimension-out-of-bounds':
|
|
46
|
+
return 'downscale source under device caps maxDimension';
|
|
47
|
+
case 'image-meta-missing':
|
|
48
|
+
return 'run forgeax-engine-remote-asset import <path>';
|
|
49
|
+
case 'image-hdr-decode-failed':
|
|
50
|
+
return 'verify Radiance RGBE header and FORMAT=32-bit_rle_rgbe field';
|
|
51
|
+
case 'atlas-empty-input':
|
|
52
|
+
return 'verify forgeax-engine-remote-asset atlas --input glob matches at least 1 PNG';
|
|
53
|
+
case 'atlas-size-exceeded':
|
|
54
|
+
return 'downscale source or split atlas under maxAtlasSize cap';
|
|
55
|
+
case 'atlas-region-mismatch':
|
|
56
|
+
return 'shelfPack regions exceed atlas footprint -- packer safety net';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
expectTypeOf(recover).toBeFunction();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('ImageErrorDetail narrows per .code via discriminated union pattern', () => {
|
|
63
|
+
function describe_(detail: ImageErrorDetail): string {
|
|
64
|
+
switch (detail.code) {
|
|
65
|
+
case 'image-decode-failed':
|
|
66
|
+
return detail.reason;
|
|
67
|
+
case 'image-format-unsupported':
|
|
68
|
+
return detail.actualMime;
|
|
69
|
+
case 'image-dimension-out-of-bounds':
|
|
70
|
+
return `${detail.requested.width}x${detail.requested.height} > ${detail.limit}`;
|
|
71
|
+
case 'image-meta-missing':
|
|
72
|
+
return `${detail.sourcePath} -> ${detail.expectedSidecarPath}`;
|
|
73
|
+
case 'image-hdr-decode-failed':
|
|
74
|
+
return detail.reason;
|
|
75
|
+
case 'atlas-empty-input':
|
|
76
|
+
return `atlas-empty:${detail.receivedCount}`;
|
|
77
|
+
case 'atlas-size-exceeded':
|
|
78
|
+
return `atlas-size:${detail.name}:${detail.width}x${detail.height}>${detail.maxAtlasSize}`;
|
|
79
|
+
case 'atlas-region-mismatch':
|
|
80
|
+
return `atlas-region:${detail.name}:${detail.regionsTotalPixels}>${detail.atlasPixels}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
expectTypeOf(describe_).toBeFunction();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('ImageError correlates envelope code and detail shape', () => {
|
|
87
|
+
function describeError(error: ImageError): string {
|
|
88
|
+
switch (error.code) {
|
|
89
|
+
case 'image-decode-failed':
|
|
90
|
+
return error.detail.reason;
|
|
91
|
+
case 'image-format-unsupported':
|
|
92
|
+
return error.detail.actualMime;
|
|
93
|
+
case 'image-dimension-out-of-bounds':
|
|
94
|
+
return `${error.detail.requested.width} > ${error.detail.limit}`;
|
|
95
|
+
case 'image-meta-missing':
|
|
96
|
+
return error.detail.expectedSidecarPath;
|
|
97
|
+
case 'image-hdr-decode-failed':
|
|
98
|
+
return error.detail.reason;
|
|
99
|
+
case 'atlas-empty-input':
|
|
100
|
+
return `${error.detail.receivedCount}`;
|
|
101
|
+
case 'atlas-size-exceeded':
|
|
102
|
+
return `${error.detail.name}:${error.detail.maxAtlasSize}`;
|
|
103
|
+
case 'atlas-region-mismatch':
|
|
104
|
+
return `${error.detail.name}:${error.detail.atlasPixels}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
expectTypeOf(describeError).toBeFunction();
|
|
109
|
+
expectTypeOf<ImageErrorDetailFor<'image-decode-failed'>>().toMatchTypeOf<{
|
|
110
|
+
code: 'image-decode-failed';
|
|
111
|
+
reason: string;
|
|
112
|
+
}>();
|
|
113
|
+
expectTypeOf<ImageErrorFor<'image-format-unsupported'>>().toMatchTypeOf<{
|
|
114
|
+
code: 'image-format-unsupported';
|
|
115
|
+
detail: { actualMime: string };
|
|
116
|
+
}>();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('ImageMeta POD has the 5 free-form fields (guid + 4 importer settings)', () => {
|
|
120
|
+
expectTypeOf<ImageMeta>().toMatchTypeOf<{
|
|
121
|
+
guid: string;
|
|
122
|
+
colorSpace: 'srgb' | 'linear';
|
|
123
|
+
mipmap: 'auto' | 'none';
|
|
124
|
+
addressMode: 'repeat' | 'clamp-to-edge' | 'mirror-repeat';
|
|
125
|
+
filterMode: 'nearest' | 'linear';
|
|
126
|
+
}>();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('DecodedImage POD has the 6 fields (bytes + dims + mime + colorSpace + mipmap)', () => {
|
|
130
|
+
expectTypeOf<DecodedImage>().toMatchTypeOf<{
|
|
131
|
+
bytes: Uint8Array;
|
|
132
|
+
width: number;
|
|
133
|
+
height: number;
|
|
134
|
+
mime: 'image/jpeg' | 'image/png' | 'image/x-tga';
|
|
135
|
+
colorSpace: 'srgb' | 'linear';
|
|
136
|
+
mipmap: boolean;
|
|
137
|
+
}>();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('ImageError 4-field surface (.code / .expected / .hint / .detail) parallels RhiError shape', () => {
|
|
141
|
+
expectTypeOf<ImageError>().toMatchTypeOf<{
|
|
142
|
+
code: ImageErrorCode;
|
|
143
|
+
expected: string;
|
|
144
|
+
hint: string;
|
|
145
|
+
detail: ImageErrorDetail;
|
|
146
|
+
}>();
|
|
147
|
+
});
|
|
148
|
+
});
|