@forgeax/engine-image 0.1.2
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 +168 -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__/ktx2-encode-mode-owner.test-d.d.ts +2 -0
- package/dist/__tests__/ktx2-encode-mode-owner.test-d.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__/runtime-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-decode.unit.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/__tests__/tga.unit.test.d.ts +2 -0
- package/dist/__tests__/tga.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 +590 -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 +15 -0
- package/dist/image-importer.d.ts.map +1 -0
- package/dist/image-importer.mjs +1107 -0
- package/dist/image-importer.mjs.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +503 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ktx2-encode.d.ts +84 -0
- package/dist/ktx2-encode.d.ts.map +1 -0
- package/dist/ktx2-encode.mjs +100 -0
- package/dist/ktx2-encode.mjs.map +1 -0
- package/dist/parse-image.d.ts +48 -0
- package/dist/parse-image.d.ts.map +1 -0
- package/dist/parse-image.mjs +231 -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/__tests__/asset-decoders.unit.test.d.ts +2 -0
- package/dist/runtime/__tests__/asset-decoders.unit.test.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/runtime/decode-image-bytes.d.ts +7 -0
- package/dist/runtime/decode-image-bytes.d.ts.map +1 -0
- package/dist/runtime/image-error.d.ts +3 -0
- package/dist/runtime/image-error.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 +1387 -0
- package/src/__tests__/ktx2-basis-importer.unit.test.ts +126 -0
- package/src/__tests__/ktx2-encode-mode-owner.test-d.ts +70 -0
- package/src/__tests__/make-fixture.ts +100 -0
- package/src/__tests__/parse-image-downscale.test.ts +26 -0
- package/src/__tests__/runtime-decode.unit.test.ts +22 -0
- package/src/__tests__/source-key.unit.test.ts +65 -0
- package/src/__tests__/tga.unit.test.ts +59 -0
- package/src/decode-image-from-file.ts +298 -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 +780 -0
- package/src/index.ts +46 -0
- package/src/ktx2-encode.ts +221 -0
- package/src/parse-image.ts +276 -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/__tests__/asset-decoders.unit.test.ts +66 -0
- package/src/runtime/asset-decoders.ts +152 -0
- package/src/runtime/decode-image-bytes.ts +41 -0
- package/src/runtime/image-error.ts +8 -0
- package/src/source-key.ts +41 -0
- package/src/sub-asset-key.ts +67 -0
- package/src/to-asset-pack.ts +95 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resize-image.d.ts","sourceRoot":"","sources":["../src/resize-image.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,wBAAgB,aAAa,CAC3B,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GACnB;IAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAgCjF"}
|
package/dist/result.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,GAAG,EACH,EAAE,EACF,KAAK,MAAM,EACX,KAAK,SAAS,EACd,KAAK,QAAQ,GACd,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-decoders.unit.test.d.ts","sourceRoot":"","sources":["../../../src/runtime/__tests__/asset-decoders.unit.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type AssetDecoderContribution, type EquirectAsset, type TextureAsset } from '@forgeax/engine-types';
|
|
2
|
+
export declare const textureContribution: AssetDecoderContribution<TextureAsset, 'texture'>;
|
|
3
|
+
export declare const equirectContribution: AssetDecoderContribution<EquirectAsset, 'equirect'>;
|
|
4
|
+
//# sourceMappingURL=asset-decoders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-decoders.d.ts","sourceRoot":"","sources":["../../src/runtime/asset-decoders.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,wBAAwB,EAK7B,KAAK,aAAa,EAGlB,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAkH/B,eAAO,MAAM,mBAAmB,EAAE,wBAAwB,CAAC,YAAY,EAAE,SAAS,CAYjF,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,wBAAwB,CAAC,aAAa,EAAE,UAAU,CAYpF,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ImageError, Result, TextureAsset } from '@forgeax/engine-types';
|
|
2
|
+
/** Image-owned runtime boundary from encoded bytes to a TextureAsset POD. */
|
|
3
|
+
export declare function decodeImageBytes(bytes: Uint8Array | ArrayBuffer, mime: string, opts?: {
|
|
4
|
+
readonly colorSpace?: 'srgb' | 'linear';
|
|
5
|
+
readonly mipmap?: boolean;
|
|
6
|
+
}): Promise<Result<TextureAsset, ImageError>>;
|
|
7
|
+
//# sourceMappingURL=decode-image-bytes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode-image-bytes.d.ts","sourceRoot":"","sources":["../../src/runtime/decode-image-bytes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAgB9E,6EAA6E;AAC7E,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,UAAU,GAAG,WAAW,EAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GAChF,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC,CAmB3C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-error.d.ts","sourceRoot":"","sources":["../../src/runtime/image-error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGhG,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,cAAc,EACxD,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC7B,aAAa,CAAC,CAAC,CAAC,CAElB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the semantic identity for an image producer output.
|
|
3
|
+
*
|
|
4
|
+
* The role is the producer-owned identity. Source path and sourceIndex are
|
|
5
|
+
* locators only and must not be folded into this key.
|
|
6
|
+
*/
|
|
7
|
+
export interface ImageSourceKeyLocator {
|
|
8
|
+
readonly sourcePath?: string;
|
|
9
|
+
readonly sourceIndex?: number;
|
|
10
|
+
}
|
|
11
|
+
export type ImageSourceKeyResult = {
|
|
12
|
+
readonly ok: true;
|
|
13
|
+
readonly keys: readonly string[];
|
|
14
|
+
} | {
|
|
15
|
+
readonly ok: false;
|
|
16
|
+
readonly code: 'missing-source-key' | 'duplicate-source-key';
|
|
17
|
+
readonly roles: readonly string[];
|
|
18
|
+
};
|
|
19
|
+
export declare function deriveImageSourceKey(role: string, _locator?: ImageSourceKeyLocator): string | undefined;
|
|
20
|
+
export declare function deriveImageSourceKeys(roles: readonly string[]): ImageSourceKeyResult;
|
|
21
|
+
//# sourceMappingURL=source-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-key.d.ts","sourceRoot":"","sources":["../src/source-key.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GACvD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,oBAAoB,GAAG,sBAAsB,CAAC;IAC7D,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC,CAAC;AAEN,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,qBAAqB,GAC/B,MAAM,GAAG,SAAS,CAIpB;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,oBAAoB,CAYpF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-asset-key shape mirroring the in-flight gltf-loader feat (plan-strategy
|
|
3
|
+
* section 2.2 D-4 same-shape). Each sub-asset emitted into a `*.meta.json`
|
|
4
|
+
* sidecar (uniformly `<source>.meta.json` across image / gltf arms,
|
|
5
|
+
* dispatched on top-level `importer` field per feat-20260521) carries a
|
|
6
|
+
* `{kind, name?, indexFallback}` triple so the importer can deterministically
|
|
7
|
+
* match an existing sub-asset across reimports (charter P5 producer/consumer
|
|
8
|
+
* split + AC-14 cross-feat alignment).
|
|
9
|
+
*
|
|
10
|
+
* The image disk schema is currently single-sub-asset; this file collapses
|
|
11
|
+
* to `kind='texture'` + `indexFallback='textures/0'` so the same matching
|
|
12
|
+
* algorithm is reusable when the future cubemap / array-layer feat lands
|
|
13
|
+
* (plan-strategy R5 free-form schema).
|
|
14
|
+
*/
|
|
15
|
+
export interface SubAssetKey {
|
|
16
|
+
/** Discriminator literal -- 'texture' for engine-image; 'mesh' / 'material' / 'scene' / 'image' for gltf-loader. */
|
|
17
|
+
readonly kind: string;
|
|
18
|
+
/** Optional human-readable name field; absent when the source has no symbolic identifier. */
|
|
19
|
+
readonly name?: string;
|
|
20
|
+
/** Path-style fallback identifier (e.g. 'images/0' / 'meshes/0'); always non-empty. */
|
|
21
|
+
readonly indexFallback: string;
|
|
22
|
+
}
|
|
23
|
+
export interface SubAssetKeyInput {
|
|
24
|
+
readonly kind: string;
|
|
25
|
+
readonly sourceIndex: number;
|
|
26
|
+
readonly name?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Construct a sub-asset key from importer input. The `indexFallback` is
|
|
30
|
+
* synthesised from the kind literal + sourceIndex so two importers
|
|
31
|
+
* (image / gltf-loader) emit identical fallback strings for the same
|
|
32
|
+
* `(kind, sourceIndex)` pair (AC-14).
|
|
33
|
+
*/
|
|
34
|
+
export declare function subAssetKey(input: SubAssetKeyInput): SubAssetKey;
|
|
35
|
+
/**
|
|
36
|
+
* Two-phase equality predicate used by reimportReuseMeta for GUID
|
|
37
|
+
* preservation across reimports:
|
|
38
|
+
*
|
|
39
|
+
* Phase 1 -- (kind + name + idx) full match: identical in all three fields
|
|
40
|
+
* Phase 2 -- (kind + idx) match: same kind + indexFallback, name absent or
|
|
41
|
+
* equal (collapses when both sides omit name)
|
|
42
|
+
* Phase 3 -- otherwise: false (importer mints fresh UUIDv7)
|
|
43
|
+
*
|
|
44
|
+
* The two phases are folded into a single boolean predicate here; the
|
|
45
|
+
* reimportReuseMeta caller iterates the existing subAssets list and picks
|
|
46
|
+
* the first hit (deterministic order; matches AC-16 byte-identical
|
|
47
|
+
* reimport).
|
|
48
|
+
*/
|
|
49
|
+
export declare function subAssetKeyEqual(a: SubAssetKey, b: SubAssetKey): boolean;
|
|
50
|
+
//# sourceMappingURL=sub-asset-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sub-asset-key.d.ts","sourceRoot":"","sources":["../src/sub-asset-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,WAAW;IAC1B,oHAAoH;IACpH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,uFAAuF;IACvF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,WAAW,CAMhE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,WAAW,GAAG,OAAO,CAQxE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AssetCompression, DecodedImage, ImageMeta } from '@forgeax/engine-types';
|
|
2
|
+
/**
|
|
3
|
+
* External-asset-package envelope shape mirroring the
|
|
4
|
+
* `packages/pack/schema/meta.schema.json` $defs/ExternalAssetPackage.
|
|
5
|
+
* Consumed by `forgeax-engine-remote-asset import` (CLI entry, M3b) +
|
|
6
|
+
* the image importer in M2 build-time path. The internal-text-package
|
|
7
|
+
* variant ($defs/InternalTextPackage) is owned by the gltf-loader feat.
|
|
8
|
+
*/
|
|
9
|
+
export interface ExternalAssetPackage {
|
|
10
|
+
readonly schemaVersion: string;
|
|
11
|
+
readonly kind: 'external-asset-package';
|
|
12
|
+
readonly importer: 'image';
|
|
13
|
+
readonly source: string;
|
|
14
|
+
readonly importSettings: ImageImportSettings;
|
|
15
|
+
readonly subAssets: readonly ExternalSubAsset[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Free-form image importer settings persisted into `*.meta.json` (importer: 'image')
|
|
19
|
+
* `importSettings`. Matches `ImageMeta` POD field-for-field (charter P5
|
|
20
|
+
* single SSOT) but the open Record shape allows future minor adds (cubemap
|
|
21
|
+
* face / array layer / custom colorSpace) without breaking existing meta
|
|
22
|
+
* files (plan-strategy R5).
|
|
23
|
+
*/
|
|
24
|
+
export interface ImageImportSettings extends Readonly<Record<string, unknown>> {
|
|
25
|
+
readonly colorSpace: ImageMeta['colorSpace'];
|
|
26
|
+
readonly mipmap: ImageMeta['mipmap'];
|
|
27
|
+
readonly addressMode: ImageMeta['addressMode'];
|
|
28
|
+
readonly filterMode: ImageMeta['filterMode'];
|
|
29
|
+
readonly downscaleMaxDimension?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Explicit per-asset compression override (AC-01: importSettings carries
|
|
32
|
+
* compression intent). When omitted, the build-time compression strategy
|
|
33
|
+
* table decides by artifact kind (mesh -> zstd, texture -> none). When set,
|
|
34
|
+
* this value wins over the default table for this asset.
|
|
35
|
+
*/
|
|
36
|
+
readonly compression?: AssetCompression;
|
|
37
|
+
}
|
|
38
|
+
export interface ExternalSubAsset {
|
|
39
|
+
readonly guid: string;
|
|
40
|
+
readonly sourceIndex: number;
|
|
41
|
+
readonly kind: string;
|
|
42
|
+
readonly name?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Pure function that translates a DecodedImage POD + ImageMeta POD into
|
|
46
|
+
* an external-asset-package envelope ready to be JSON-stringified into a
|
|
47
|
+
* `*.meta.json` sidecar with importer: 'image' (plan-strategy section 3.2 sequence A; AC-13
|
|
48
|
+
* disk schema reuse).
|
|
49
|
+
*
|
|
50
|
+
* The single sub-asset emitted carries:
|
|
51
|
+
* - `guid` -- copied verbatim from `meta.guid` (the producer guarantees
|
|
52
|
+
* the GUID is freshly minted or reused via `reimportReuseMeta`)
|
|
53
|
+
* - `sourceIndex: 0` (single-sub-asset shape; cubemap / array reserved
|
|
54
|
+
* for future feat)
|
|
55
|
+
* - `kind: 'texture'` (closed literal for image-importer emit)
|
|
56
|
+
*
|
|
57
|
+
* Two consecutive calls with identical inputs produce JSON.stringify
|
|
58
|
+
* byte-equal output (AC-16 idempotent reimport).
|
|
59
|
+
*/
|
|
60
|
+
export declare function toAssetPack(decoded: DecodedImage, meta: ImageMeta): ExternalAssetPackage;
|
|
61
|
+
//# sourceMappingURL=to-asset-pack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-asset-pack.d.ts","sourceRoot":"","sources":["../src/to-asset-pack.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvF;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,mBAAmB,CAAC;IAC7C,QAAQ,CAAC,SAAS,EAAE,SAAS,gBAAgB,EAAE,CAAC;CACjD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAoB,SAAQ,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IACrC,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC;IAC/C,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;IAC7C,QAAQ,CAAC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC;CACzC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,GAAG,oBAAoB,CA+BxF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@forgeax/engine-image",
|
|
3
|
+
"version": "0.1.2",
|
|
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.1.2",
|
|
58
|
+
"@forgeax/engine-math": "0.1.2",
|
|
59
|
+
"@forgeax/engine-pack": "0.1.2",
|
|
60
|
+
"@forgeax/engine-types": "0.1.2",
|
|
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.1.2",
|
|
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
|
+
});
|