@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.
Files changed (122) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +168 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/compression-mode.unit.test.d.ts +2 -0
  5. package/dist/__tests__/compression-mode.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/encode-source-too-large.unit.test.d.ts +2 -0
  7. package/dist/__tests__/encode-source-too-large.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/errors.test-d.d.ts +2 -0
  9. package/dist/__tests__/errors.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/image-importer-conversion-failure.unit.test.d.ts +2 -0
  11. package/dist/__tests__/image-importer-conversion-failure.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/image-importer-hdr-equirect.test.d.ts +2 -0
  13. package/dist/__tests__/image-importer-hdr-equirect.test.d.ts.map +1 -0
  14. package/dist/__tests__/image-importer-topology.unit.test.d.ts +2 -0
  15. package/dist/__tests__/image-importer-topology.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/image-importer.test.d.ts +2 -0
  17. package/dist/__tests__/image-importer.test.d.ts.map +1 -0
  18. package/dist/__tests__/image-local-artifacts.test.d.ts +2 -0
  19. package/dist/__tests__/image-local-artifacts.test.d.ts.map +1 -0
  20. package/dist/__tests__/image.unit.test.d.ts +2 -0
  21. package/dist/__tests__/image.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/ktx2-basis-importer.unit.test.d.ts +2 -0
  23. package/dist/__tests__/ktx2-basis-importer.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/ktx2-encode-mode-owner.test-d.d.ts +2 -0
  25. package/dist/__tests__/ktx2-encode-mode-owner.test-d.d.ts.map +1 -0
  26. package/dist/__tests__/make-fixture.d.ts +12 -0
  27. package/dist/__tests__/make-fixture.d.ts.map +1 -0
  28. package/dist/__tests__/parse-image-downscale.test.d.ts +2 -0
  29. package/dist/__tests__/parse-image-downscale.test.d.ts.map +1 -0
  30. package/dist/__tests__/runtime-decode.unit.test.d.ts +2 -0
  31. package/dist/__tests__/runtime-decode.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/source-key.unit.test.d.ts +2 -0
  33. package/dist/__tests__/source-key.unit.test.d.ts.map +1 -0
  34. package/dist/__tests__/tga.unit.test.d.ts +2 -0
  35. package/dist/__tests__/tga.unit.test.d.ts.map +1 -0
  36. package/dist/decode-image-from-file.d.ts +40 -0
  37. package/dist/decode-image-from-file.d.ts.map +1 -0
  38. package/dist/decode-image-from-file.mjs +590 -0
  39. package/dist/decode-image-from-file.mjs.map +1 -0
  40. package/dist/errors.d.ts +14 -0
  41. package/dist/errors.d.ts.map +1 -0
  42. package/dist/hdr-decoder.d.ts +28 -0
  43. package/dist/hdr-decoder.d.ts.map +1 -0
  44. package/dist/hdr-decoder.mjs +212 -0
  45. package/dist/hdr-decoder.mjs.map +1 -0
  46. package/dist/image-decoder-browser.d.ts +22 -0
  47. package/dist/image-decoder-browser.d.ts.map +1 -0
  48. package/dist/image-decoder-node.d.ts +50 -0
  49. package/dist/image-decoder-node.d.ts.map +1 -0
  50. package/dist/image-importer.d.ts +15 -0
  51. package/dist/image-importer.d.ts.map +1 -0
  52. package/dist/image-importer.mjs +1107 -0
  53. package/dist/image-importer.mjs.map +1 -0
  54. package/dist/index.d.ts +19 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.mjs +503 -0
  57. package/dist/index.mjs.map +1 -0
  58. package/dist/ktx2-encode.d.ts +84 -0
  59. package/dist/ktx2-encode.d.ts.map +1 -0
  60. package/dist/ktx2-encode.mjs +100 -0
  61. package/dist/ktx2-encode.mjs.map +1 -0
  62. package/dist/parse-image.d.ts +48 -0
  63. package/dist/parse-image.d.ts.map +1 -0
  64. package/dist/parse-image.mjs +231 -0
  65. package/dist/parse-image.mjs.map +1 -0
  66. package/dist/reimport-reuse-meta.d.ts +81 -0
  67. package/dist/reimport-reuse-meta.d.ts.map +1 -0
  68. package/dist/resize-image.d.ts +7 -0
  69. package/dist/resize-image.d.ts.map +1 -0
  70. package/dist/result.d.ts +2 -0
  71. package/dist/result.d.ts.map +1 -0
  72. package/dist/runtime/__tests__/asset-decoders.unit.test.d.ts +2 -0
  73. package/dist/runtime/__tests__/asset-decoders.unit.test.d.ts.map +1 -0
  74. package/dist/runtime/asset-decoders.d.ts +4 -0
  75. package/dist/runtime/asset-decoders.d.ts.map +1 -0
  76. package/dist/runtime/decode-image-bytes.d.ts +7 -0
  77. package/dist/runtime/decode-image-bytes.d.ts.map +1 -0
  78. package/dist/runtime/image-error.d.ts +3 -0
  79. package/dist/runtime/image-error.d.ts.map +1 -0
  80. package/dist/source-key.d.ts +21 -0
  81. package/dist/source-key.d.ts.map +1 -0
  82. package/dist/sub-asset-key.d.ts +50 -0
  83. package/dist/sub-asset-key.d.ts.map +1 -0
  84. package/dist/to-asset-pack.d.ts +61 -0
  85. package/dist/to-asset-pack.d.ts.map +1 -0
  86. package/package.json +105 -0
  87. package/src/__tests__/compression-mode.unit.test.ts +104 -0
  88. package/src/__tests__/encode-source-too-large.unit.test.ts +81 -0
  89. package/src/__tests__/errors.test-d.ts +148 -0
  90. package/src/__tests__/image-importer-conversion-failure.unit.test.ts +297 -0
  91. package/src/__tests__/image-importer-hdr-equirect.test.ts +218 -0
  92. package/src/__tests__/image-importer-topology.unit.test.ts +209 -0
  93. package/src/__tests__/image-importer.test.ts +12 -0
  94. package/src/__tests__/image-local-artifacts.test.ts +38 -0
  95. package/src/__tests__/image.unit.test.ts +1387 -0
  96. package/src/__tests__/ktx2-basis-importer.unit.test.ts +126 -0
  97. package/src/__tests__/ktx2-encode-mode-owner.test-d.ts +70 -0
  98. package/src/__tests__/make-fixture.ts +100 -0
  99. package/src/__tests__/parse-image-downscale.test.ts +26 -0
  100. package/src/__tests__/runtime-decode.unit.test.ts +22 -0
  101. package/src/__tests__/source-key.unit.test.ts +65 -0
  102. package/src/__tests__/tga.unit.test.ts +59 -0
  103. package/src/decode-image-from-file.ts +298 -0
  104. package/src/errors.ts +60 -0
  105. package/src/hdr-decoder.ts +255 -0
  106. package/src/image-decoder-browser.ts +88 -0
  107. package/src/image-decoder-node.ts +67 -0
  108. package/src/image-decoders.d.ts +23 -0
  109. package/src/image-importer.ts +780 -0
  110. package/src/index.ts +46 -0
  111. package/src/ktx2-encode.ts +221 -0
  112. package/src/parse-image.ts +276 -0
  113. package/src/reimport-reuse-meta.ts +160 -0
  114. package/src/resize-image.ts +39 -0
  115. package/src/result.ts +13 -0
  116. package/src/runtime/__tests__/asset-decoders.unit.test.ts +66 -0
  117. package/src/runtime/asset-decoders.ts +152 -0
  118. package/src/runtime/decode-image-bytes.ts +41 -0
  119. package/src/runtime/image-error.ts +8 -0
  120. package/src/source-key.ts +41 -0
  121. package/src/sub-asset-key.ts +67 -0
  122. 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"}
@@ -0,0 +1,2 @@
1
+ export { err, ok, type Result, type ResultErr, type ResultOk, } from '@forgeax/engine-types';
2
+ //# sourceMappingURL=result.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=asset-decoders.unit.test.d.ts.map
@@ -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,3 @@
1
+ import type { ImageErrorCode, ImageErrorDetailFor, ImageErrorFor } from '@forgeax/engine-types';
2
+ export declare function runtimeImageError<C extends ImageErrorCode>(detail: ImageErrorDetailFor<C>): ImageErrorFor<C>;
3
+ //# sourceMappingURL=image-error.d.ts.map
@@ -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
+ });