@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,780 @@
|
|
|
1
|
+
// image-importer.ts - the build-time imageImporter (feat-20260603-asset-import-loader-injection M3 / w23).
|
|
2
|
+
//
|
|
3
|
+
// The `{ key: 'image', import }` Importer the @forgeax/engine-import runner
|
|
4
|
+
// dispatches a `*.meta.json` with `importer: 'image'` to. It absorbs the
|
|
5
|
+
// decode logic that previously lived inline in
|
|
6
|
+
// `@forgeax/engine-vite-plugin-pack`'s generateBundle (research Finding 4,
|
|
7
|
+
// vite-plugin-pack/src/index.ts:354-447): read the source bytes -> parseImage
|
|
8
|
+
// -> tight-packed RGBA `DecodedImage` -> a `TextureAsset` POD whose `data`
|
|
9
|
+
// column carries the imported RGBA bytes under the meta-declared GUID.
|
|
10
|
+
//
|
|
11
|
+
// Why image-importer lives in @forgeax/engine-image (D-9): the image domain
|
|
12
|
+
// logic stays co-located with the package that already owns parseImage (a
|
|
13
|
+
// node-only decoder). Splitting the import into a third package would fracture
|
|
14
|
+
// the SSOT. This module is a NODE-ONLY sub-export
|
|
15
|
+
// (`@forgeax/engine-image/image-importer`, `default: null` under browser
|
|
16
|
+
// conditions) because it statically imports `./parse-image.js` (jpeg-js +
|
|
17
|
+
// upng-js). The browser runtime never reaches it: the texture is decoded at
|
|
18
|
+
// build time and the runtime loader reads the imported `.bin` (M3 strips the
|
|
19
|
+
// runtime decoder edge, AC-15).
|
|
20
|
+
//
|
|
21
|
+
// importSettings folding (colorSpace / mipmap -> TextureAsset.format) mirrors
|
|
22
|
+
// `build-catalog.ts` buildImageMetadata (D-5: `'auto'` -> true / `'none'` ->
|
|
23
|
+
// false, `'srgb'` -> 'rgba8unorm-srgb' / `'linear'` -> 'rgba8unorm') so the
|
|
24
|
+
// importer and the catalog builder derive the same texture metadata. The
|
|
25
|
+
// imported RGBA bytes ride in `TextureAsset.data`; the generateBundle integration (w28)
|
|
26
|
+
// extracts that buffer into a hashed `.bin` and folds width/height/format into
|
|
27
|
+
// the pack-index row.
|
|
28
|
+
//
|
|
29
|
+
// GUID import-stable iron law: every produced `ImportedAsset.guid` comes from
|
|
30
|
+
// `ctx.subAssets[]`, never minted here. A sub-asset of `kind: 'equirect'`
|
|
31
|
+
// (HDR lat-long env map) IS folded by the .hdr arm into an EquirectAsset POD
|
|
32
|
+
// (a single 2D rgba16float image); the cube-to-cube IBL projection is a runtime
|
|
33
|
+
// GPU pass, not a build-time fold (feat-20260630).
|
|
34
|
+
|
|
35
|
+
import { type BasisSourceInspection, ktx2ColorSpace, parseKtx2 } from '@forgeax/engine-codec';
|
|
36
|
+
import type {
|
|
37
|
+
EquirectAsset,
|
|
38
|
+
ImageColorSpace,
|
|
39
|
+
ImportContext,
|
|
40
|
+
ImportedAsset,
|
|
41
|
+
Importer,
|
|
42
|
+
ImportResult,
|
|
43
|
+
TextureAsset,
|
|
44
|
+
} from '@forgeax/engine-types';
|
|
45
|
+
import { IMPORT_ERROR_HINTS, ImportError } from '@forgeax/engine-types';
|
|
46
|
+
import type { CompressionMode } from './ktx2-encode.js';
|
|
47
|
+
import { encodeTextureToKtx2, resolveEncodeMode } from './ktx2-encode.js';
|
|
48
|
+
import { parseImage } from './parse-image.js';
|
|
49
|
+
|
|
50
|
+
/** Map a source path / mime hint to the parseImage mime literal. */
|
|
51
|
+
function mimeFromSource(source: string): 'image/png' | 'image/jpeg' | 'image/x-tga' | undefined {
|
|
52
|
+
const lower = source.toLowerCase();
|
|
53
|
+
if (lower.endsWith('.png')) return 'image/png';
|
|
54
|
+
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
|
|
55
|
+
if (lower.endsWith('.tga')) return 'image/x-tga';
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type RequiredImageOutputKind = 'texture' | 'equirect';
|
|
60
|
+
|
|
61
|
+
function requiredImageOutputKind(source: string): RequiredImageOutputKind | undefined {
|
|
62
|
+
const lower = source.toLowerCase();
|
|
63
|
+
if (lower.endsWith('.hdr')) return 'equirect';
|
|
64
|
+
if (mimeFromSource(source) !== undefined || lower.endsWith('.basis') || lower.endsWith('.ktx2')) {
|
|
65
|
+
return 'texture';
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function imageOutputTopologyActual(ctx: ImportContext): string {
|
|
71
|
+
if (ctx.subAssets.length === 0) return 'subAssets[] is empty';
|
|
72
|
+
return ctx.subAssets
|
|
73
|
+
.map(
|
|
74
|
+
(sub, index) => `subAssets[${index}]=${sub.kind}:${sub.guid}:sourceIndex=${sub.sourceIndex}`,
|
|
75
|
+
)
|
|
76
|
+
.join(', ');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type ImageConversionStage = 'decode' | 'inspect' | 'encode';
|
|
80
|
+
|
|
81
|
+
function imageConversionFailure(
|
|
82
|
+
ctx: ImportContext,
|
|
83
|
+
stage: ImageConversionStage,
|
|
84
|
+
ownerCode: string,
|
|
85
|
+
sourcePath: string,
|
|
86
|
+
expected: string,
|
|
87
|
+
actual: string,
|
|
88
|
+
hint: string,
|
|
89
|
+
): ImportError {
|
|
90
|
+
const diagnosticSourcePath = sourcePath.length === 0 ? ctx.source : sourcePath;
|
|
91
|
+
return new ImportError({
|
|
92
|
+
code: 'source-validation-failed',
|
|
93
|
+
expected,
|
|
94
|
+
actual,
|
|
95
|
+
hint: IMPORT_ERROR_HINTS['source-validation-failed'],
|
|
96
|
+
detail: {
|
|
97
|
+
diagnostics: [
|
|
98
|
+
{
|
|
99
|
+
code: `image-conversion-${stage}-${ownerCode}`,
|
|
100
|
+
severity: 'error',
|
|
101
|
+
sourcePath: diagnosticSourcePath,
|
|
102
|
+
sourceRange: { start: 0, end: 0, line: 1, column: 1 },
|
|
103
|
+
rule: `image-conversion-${stage}`,
|
|
104
|
+
expected,
|
|
105
|
+
actual,
|
|
106
|
+
hint,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function validateImageOutputTopology(
|
|
114
|
+
ctx: ImportContext,
|
|
115
|
+
requiredKind: RequiredImageOutputKind,
|
|
116
|
+
): ImportError | undefined {
|
|
117
|
+
if (
|
|
118
|
+
ctx.subAssets.length === 1 &&
|
|
119
|
+
ctx.subAssets[0]?.kind === requiredKind &&
|
|
120
|
+
ctx.subAssets[0]?.sourceIndex === 0
|
|
121
|
+
) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const expected = `exactly one subAssets[] entry with kind "${requiredKind}" and sourceIndex 0`;
|
|
126
|
+
const actual = imageOutputTopologyActual(ctx);
|
|
127
|
+
return new ImportError({
|
|
128
|
+
code: 'source-validation-failed',
|
|
129
|
+
expected,
|
|
130
|
+
actual,
|
|
131
|
+
hint: IMPORT_ERROR_HINTS['source-validation-failed'],
|
|
132
|
+
detail: {
|
|
133
|
+
diagnostics: [
|
|
134
|
+
{
|
|
135
|
+
code: 'image-subasset-topology',
|
|
136
|
+
severity: 'error',
|
|
137
|
+
sourcePath: `${ctx.source}#subAssets`,
|
|
138
|
+
sourceRange: { start: 0, end: 0, line: 1, column: 1 },
|
|
139
|
+
rule: 'image-required-single-output',
|
|
140
|
+
expected,
|
|
141
|
+
actual,
|
|
142
|
+
hint: `declare exactly one ${requiredKind} sub-asset with sourceIndex 0 and remove foreign, duplicate, or misplaced entries`,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** D-5 mipmap token mapping (mirrors build-catalog mipmapTokenToBoolean). */
|
|
150
|
+
function mipmapTokenToBoolean(token: unknown): boolean {
|
|
151
|
+
return token === 'auto' || token === true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** colorSpace -> GPU format literal (mirrors build-catalog colorSpaceToFormat). */
|
|
155
|
+
function colorSpaceToFormat(colorSpace: ImageColorSpace): GPUTextureFormat {
|
|
156
|
+
return colorSpace === 'srgb' ? 'rgba8unorm-srgb' : 'rgba8unorm';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type BasisProfile = 'etc1s' | 'uastc-ldr' | 'uastc-hdr';
|
|
160
|
+
|
|
161
|
+
async function inspectKtx2Source(
|
|
162
|
+
ctx: ImportContext,
|
|
163
|
+
bytes: Uint8Array,
|
|
164
|
+
metaColorSpace: ImageColorSpace | undefined,
|
|
165
|
+
): Promise<
|
|
166
|
+
| {
|
|
167
|
+
readonly colorSpace: ImageColorSpace;
|
|
168
|
+
readonly profile: BasisProfile;
|
|
169
|
+
readonly width: number;
|
|
170
|
+
readonly height: number;
|
|
171
|
+
readonly levelCount: number;
|
|
172
|
+
}
|
|
173
|
+
| ImportError
|
|
174
|
+
> {
|
|
175
|
+
const parsed = await parseKtx2(bytes);
|
|
176
|
+
if (!parsed.ok) {
|
|
177
|
+
return imageConversionFailure(
|
|
178
|
+
ctx,
|
|
179
|
+
'inspect',
|
|
180
|
+
'ktx2-parse',
|
|
181
|
+
ctx.source,
|
|
182
|
+
'a valid KTX2 container with a supported Basis profile',
|
|
183
|
+
`codec:${parsed.error.code}`,
|
|
184
|
+
'repair the KTX2 container and retry the image import',
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const dfdColorSpace = ktx2ColorSpace(parsed.value);
|
|
188
|
+
if (dfdColorSpace === undefined) {
|
|
189
|
+
return imageConversionFailure(
|
|
190
|
+
ctx,
|
|
191
|
+
'inspect',
|
|
192
|
+
'ktx2-color-space-missing',
|
|
193
|
+
`${ctx.source}#DFD.transferFunction`,
|
|
194
|
+
'DFD transfer function to identify sRGB or linear color space',
|
|
195
|
+
'missing-or-unsupported-transfer-function',
|
|
196
|
+
'repair the KTX2 DFD color-space declaration before importing',
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (metaColorSpace !== undefined && metaColorSpace !== dfdColorSpace) {
|
|
200
|
+
return imageConversionFailure(
|
|
201
|
+
ctx,
|
|
202
|
+
'inspect',
|
|
203
|
+
'ktx2-color-space-conflict',
|
|
204
|
+
`${ctx.source}#importSettings.colorSpace`,
|
|
205
|
+
'Meta.colorSpace to match the KTX2 DFD transfer function',
|
|
206
|
+
`meta=${metaColorSpace},dfd=${dfdColorSpace}`,
|
|
207
|
+
'repair Meta.colorSpace or re-encode the KTX2 source with matching color provenance',
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const { pixelDepth, layerCount, faceCount } = parsed.value.header;
|
|
211
|
+
if (pixelDepth !== 0 || layerCount > 1 || faceCount !== 1) {
|
|
212
|
+
return imageConversionFailure(
|
|
213
|
+
ctx,
|
|
214
|
+
'inspect',
|
|
215
|
+
'ktx2-shape-unsupported',
|
|
216
|
+
`${ctx.source}#header`,
|
|
217
|
+
'a 2D, single-layer, single-face KTX2 texture',
|
|
218
|
+
`pixelDepth=${pixelDepth},layerCount=${layerCount},faceCount=${faceCount}`,
|
|
219
|
+
're-encode the source as one 2D Basis texture',
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const { initBasisTranscoder } = await import('@forgeax/engine-codec');
|
|
223
|
+
const module = await initBasisTranscoder();
|
|
224
|
+
let file: InstanceType<typeof module.KTX2File>;
|
|
225
|
+
try {
|
|
226
|
+
file = new module.KTX2File(bytes);
|
|
227
|
+
} catch {
|
|
228
|
+
return imageConversionFailure(
|
|
229
|
+
ctx,
|
|
230
|
+
'inspect',
|
|
231
|
+
'ktx2-invalid',
|
|
232
|
+
ctx.source,
|
|
233
|
+
'Basis KTX2File bytes to pass transcoder validation',
|
|
234
|
+
'basis-file-constructor-failed',
|
|
235
|
+
'repair or re-encode the KTX2 source before importing',
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
try {
|
|
240
|
+
if (!file.isValid()) {
|
|
241
|
+
return imageConversionFailure(
|
|
242
|
+
ctx,
|
|
243
|
+
'inspect',
|
|
244
|
+
'ktx2-invalid',
|
|
245
|
+
ctx.source,
|
|
246
|
+
'Basis KTX2File bytes to pass transcoder validation',
|
|
247
|
+
'basis-file-invalid',
|
|
248
|
+
'repair or re-encode the KTX2 source before importing',
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const profile: BasisProfile | undefined = file.isETC1S()
|
|
252
|
+
? 'etc1s'
|
|
253
|
+
: file.isHDR() || file.isHDR4x4()
|
|
254
|
+
? 'uastc-hdr'
|
|
255
|
+
: file.isUASTC_LDR_4x4()
|
|
256
|
+
? 'uastc-ldr'
|
|
257
|
+
: undefined;
|
|
258
|
+
if (profile === undefined) {
|
|
259
|
+
return imageConversionFailure(
|
|
260
|
+
ctx,
|
|
261
|
+
'inspect',
|
|
262
|
+
'ktx2-profile-unsupported',
|
|
263
|
+
`${ctx.source}#Basis.profile`,
|
|
264
|
+
'ETC1S, UASTC-LDR, or UASTC-HDR Basis profile',
|
|
265
|
+
'unsupported-basis-profile',
|
|
266
|
+
're-encode the source with ETC1S, UASTC-LDR, or UASTC-HDR',
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (profile === 'uastc-hdr' && dfdColorSpace !== 'linear') {
|
|
270
|
+
return imageConversionFailure(
|
|
271
|
+
ctx,
|
|
272
|
+
'inspect',
|
|
273
|
+
'ktx2-hdr-color-space-invalid',
|
|
274
|
+
`${ctx.source}#DFD.transferFunction`,
|
|
275
|
+
'UASTC-HDR source to use linear color space',
|
|
276
|
+
`profile=${profile},dfd=${dfdColorSpace}`,
|
|
277
|
+
're-encode HDR data with a linear KTX2 DFD transfer function',
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
colorSpace: dfdColorSpace,
|
|
282
|
+
profile,
|
|
283
|
+
width: file.getWidth(),
|
|
284
|
+
height: file.getHeight(),
|
|
285
|
+
levelCount: file.getLevels(),
|
|
286
|
+
};
|
|
287
|
+
} catch {
|
|
288
|
+
return imageConversionFailure(
|
|
289
|
+
ctx,
|
|
290
|
+
'inspect',
|
|
291
|
+
'ktx2-invalid',
|
|
292
|
+
ctx.source,
|
|
293
|
+
'Basis KTX2File bytes to pass transcoder inspection',
|
|
294
|
+
'basis-inspection-threw',
|
|
295
|
+
'repair or re-encode the KTX2 source before importing',
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
} finally {
|
|
299
|
+
file.close();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function importKtx2Source(ctx: ImportContext, bytes: Uint8Array): Promise<ImportResult> {
|
|
304
|
+
const sourceKey = ctx.subAssets[0]?.sourceKey;
|
|
305
|
+
const metaColorSpace =
|
|
306
|
+
sourceKey === undefined
|
|
307
|
+
? ctx.importSettings.colorSpace
|
|
308
|
+
: ctx.sourceOverrides?.[sourceKey]?.colorSpace;
|
|
309
|
+
const inspection = await inspectKtx2Source(
|
|
310
|
+
ctx,
|
|
311
|
+
bytes,
|
|
312
|
+
metaColorSpace === 'srgb' || metaColorSpace === 'linear' ? metaColorSpace : undefined,
|
|
313
|
+
);
|
|
314
|
+
if (inspection instanceof ImportError) return { ok: false, error: inspection };
|
|
315
|
+
const out: ImportedAsset[] = [];
|
|
316
|
+
for (const sub of ctx.subAssets) {
|
|
317
|
+
if (sub.kind !== 'texture' || sub.sourceIndex !== 0) continue;
|
|
318
|
+
out.push({
|
|
319
|
+
guid: sub.guid,
|
|
320
|
+
kind: 'texture',
|
|
321
|
+
payload: {
|
|
322
|
+
kind: 'texture',
|
|
323
|
+
width: inspection.width,
|
|
324
|
+
height: inspection.height,
|
|
325
|
+
format: colorSpaceToFormat(inspection.colorSpace),
|
|
326
|
+
data: bytes,
|
|
327
|
+
colorSpace: inspection.colorSpace,
|
|
328
|
+
mipmap: inspection.levelCount > 1,
|
|
329
|
+
mipLevelCount: inspection.levelCount,
|
|
330
|
+
},
|
|
331
|
+
refs: [],
|
|
332
|
+
artifacts: {
|
|
333
|
+
body: {
|
|
334
|
+
mediaType: 'image/ktx2',
|
|
335
|
+
assetCodec: {
|
|
336
|
+
name: 'basis',
|
|
337
|
+
container: 'ktx2',
|
|
338
|
+
profile: inspection.profile,
|
|
339
|
+
version: '1',
|
|
340
|
+
},
|
|
341
|
+
bytes,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return { ok: true, value: { assets: out, sourceDependencies: [] } };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function basisMetaColorSpace(ctx: ImportContext): ImageColorSpace | ImportError {
|
|
350
|
+
const sourceKey = ctx.subAssets[0]?.sourceKey;
|
|
351
|
+
const override = sourceKey === undefined ? undefined : ctx.sourceOverrides?.[sourceKey];
|
|
352
|
+
const value = override?.colorSpace ?? ctx.importSettings.colorSpace;
|
|
353
|
+
if (value !== 'srgb' && value !== 'linear') {
|
|
354
|
+
return imageConversionFailure(
|
|
355
|
+
ctx,
|
|
356
|
+
'inspect',
|
|
357
|
+
'basis-color-space-invalid',
|
|
358
|
+
`${ctx.source}#Meta.colorSpace`,
|
|
359
|
+
'Meta.colorSpace to be srgb or linear for raw Basis',
|
|
360
|
+
value === undefined ? 'missing' : `invalid:${String(value)}`,
|
|
361
|
+
'set Meta.colorSpace to srgb or linear before importing raw Basis bytes',
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
return value;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function importBasisSource(ctx: ImportContext, bytes: Uint8Array): Promise<ImportResult> {
|
|
368
|
+
const colorSpace = basisMetaColorSpace(ctx);
|
|
369
|
+
if (colorSpace instanceof ImportError) return { ok: false, error: colorSpace };
|
|
370
|
+
const { initBasisTranscoder, inspectBasisSource } = await import('@forgeax/engine-codec');
|
|
371
|
+
const module = await initBasisTranscoder();
|
|
372
|
+
if (module.BasisFile === undefined) {
|
|
373
|
+
throw new Error('basis-source-inspection-unavailable: transcoder lacks BasisFile');
|
|
374
|
+
}
|
|
375
|
+
let file: InstanceType<typeof module.BasisFile>;
|
|
376
|
+
try {
|
|
377
|
+
file = new module.BasisFile(bytes);
|
|
378
|
+
} catch {
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
error: imageConversionFailure(
|
|
382
|
+
ctx,
|
|
383
|
+
'inspect',
|
|
384
|
+
'basis-source-invalid',
|
|
385
|
+
ctx.source,
|
|
386
|
+
'raw Basis bytes to pass transcoder validation',
|
|
387
|
+
'basis-file-invalid',
|
|
388
|
+
'repair or re-encode the raw Basis source before importing',
|
|
389
|
+
),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
let inspection: BasisSourceInspection | undefined;
|
|
393
|
+
try {
|
|
394
|
+
try {
|
|
395
|
+
const format = file.getBasisTexFormat();
|
|
396
|
+
const profile =
|
|
397
|
+
format === module.basis_tex_format.cETC1S.value
|
|
398
|
+
? 'etc1s'
|
|
399
|
+
: format === module.basis_tex_format.cUASTC_LDR_4x4.value
|
|
400
|
+
? 'uastc-ldr'
|
|
401
|
+
: undefined;
|
|
402
|
+
if (profile === undefined) {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
error: imageConversionFailure(
|
|
406
|
+
ctx,
|
|
407
|
+
'inspect',
|
|
408
|
+
'basis-profile-unsupported',
|
|
409
|
+
`${ctx.source}#Basis.profile`,
|
|
410
|
+
'ETC1S or UASTC-LDR raw Basis profile',
|
|
411
|
+
'unsupported-basis-profile',
|
|
412
|
+
're-encode the source with ETC1S or UASTC-LDR',
|
|
413
|
+
),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const result = inspectBasisSource(file, { colorSpace }, profile);
|
|
417
|
+
if (!result.ok) {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
error: imageConversionFailure(
|
|
421
|
+
ctx,
|
|
422
|
+
'inspect',
|
|
423
|
+
'basis-inspection-failed',
|
|
424
|
+
ctx.source,
|
|
425
|
+
'raw Basis source metadata to pass codec inspection',
|
|
426
|
+
`codec:${result.error.code}`,
|
|
427
|
+
'repair the raw Basis source metadata and retry the import',
|
|
428
|
+
),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
inspection = result.value;
|
|
432
|
+
} catch {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
error: imageConversionFailure(
|
|
436
|
+
ctx,
|
|
437
|
+
'inspect',
|
|
438
|
+
'basis-source-invalid',
|
|
439
|
+
ctx.source,
|
|
440
|
+
'raw Basis bytes to pass transcoder inspection',
|
|
441
|
+
'basis-inspection-threw',
|
|
442
|
+
'repair or re-encode the raw Basis source before importing',
|
|
443
|
+
),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
} finally {
|
|
447
|
+
file.close();
|
|
448
|
+
}
|
|
449
|
+
if (inspection === undefined) {
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
error: imageConversionFailure(
|
|
453
|
+
ctx,
|
|
454
|
+
'inspect',
|
|
455
|
+
'basis-inspection-failed',
|
|
456
|
+
ctx.source,
|
|
457
|
+
'raw Basis source metadata to be available after codec inspection',
|
|
458
|
+
'missing-inspection-result',
|
|
459
|
+
'repair the raw Basis source and retry the import',
|
|
460
|
+
),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const out: ImportedAsset[] = [];
|
|
464
|
+
for (const sub of ctx.subAssets) {
|
|
465
|
+
if (sub.kind !== 'texture') continue;
|
|
466
|
+
if (sub.sourceIndex !== 0 || inspection.imageCount !== 1) {
|
|
467
|
+
return {
|
|
468
|
+
ok: false,
|
|
469
|
+
error: imageConversionFailure(
|
|
470
|
+
ctx,
|
|
471
|
+
'inspect',
|
|
472
|
+
'basis-image-shape-unsupported',
|
|
473
|
+
`${ctx.source}#Basis.images`,
|
|
474
|
+
'one raw Basis image at sourceIndex 0',
|
|
475
|
+
`imageCount=${inspection.imageCount},sourceIndex=${sub.sourceIndex}`,
|
|
476
|
+
're-encode the source as one raw Basis image',
|
|
477
|
+
),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
out.push({
|
|
481
|
+
guid: sub.guid,
|
|
482
|
+
kind: 'texture',
|
|
483
|
+
payload: {
|
|
484
|
+
kind: 'texture',
|
|
485
|
+
width: inspection.width,
|
|
486
|
+
height: inspection.height,
|
|
487
|
+
format: colorSpaceToFormat(colorSpace),
|
|
488
|
+
data: bytes,
|
|
489
|
+
colorSpace,
|
|
490
|
+
mipmap: inspection.levelCount > 1,
|
|
491
|
+
mipLevelCount: inspection.levelCount,
|
|
492
|
+
},
|
|
493
|
+
refs: [],
|
|
494
|
+
artifacts: {
|
|
495
|
+
body: {
|
|
496
|
+
mediaType: 'application/x-forgeax-basis',
|
|
497
|
+
assetCodec: {
|
|
498
|
+
name: 'basis',
|
|
499
|
+
container: 'basis',
|
|
500
|
+
profile: inspection.profile,
|
|
501
|
+
version: '1',
|
|
502
|
+
},
|
|
503
|
+
bytes,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
return { ok: true, value: { assets: out, sourceDependencies: [] } };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Read the sidecar compressionMode token (D-12 / M3 w18).
|
|
513
|
+
*
|
|
514
|
+
* M3 SEQUENCING CONSTRAINT (plan R-9): the default is hard-wired to `'none'`.
|
|
515
|
+
* An absent / unrecognised token stays `'none'` so existing textures keep the
|
|
516
|
+
* uncompressed `.bin` path -- flipping the default to `'auto'` is M5 (w38).
|
|
517
|
+
*/
|
|
518
|
+
function compressionModeToken(token: unknown): CompressionMode {
|
|
519
|
+
if (token === 'auto' || token === 'etc1s' || token === 'uastc' || token === 'none') {
|
|
520
|
+
return token;
|
|
521
|
+
}
|
|
522
|
+
return 'none';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Basis encode arm (D-5 / M3 w18; HDR arm feat-20260707): when the sidecar
|
|
527
|
+
* requests a compressed delivery (mode resolves to non-'none'), encode the
|
|
528
|
+
* decoded pixels into a Basis KTX2 and return those bytes; the catalog
|
|
529
|
+
* `compression` discriminant is set by the vite-plugin-pack wiring (w20).
|
|
530
|
+
* Returns `null` for the 'none' path so the caller keeps the uncompressed
|
|
531
|
+
* `.bin` bytes unchanged (rgba8 for LDR, rgba16float for HDR).
|
|
532
|
+
*
|
|
533
|
+
* `pixels` is tight-packed RGBA: 8-bit RGBA for LDR (`isHdr: false`),
|
|
534
|
+
* rgba16float bytes for HDR (`isHdr: true`). The `isHdr` signal drives both the
|
|
535
|
+
* 'auto' derivation (-> 'uastc-hdr') and the encoder's HDR source path.
|
|
536
|
+
*/
|
|
537
|
+
async function maybeEncodeTextureBytes(
|
|
538
|
+
ctx: ImportContext,
|
|
539
|
+
pixels: Uint8Array,
|
|
540
|
+
width: number,
|
|
541
|
+
height: number,
|
|
542
|
+
compressionMode: CompressionMode,
|
|
543
|
+
colorSpace: ImageColorSpace,
|
|
544
|
+
isHdr: boolean,
|
|
545
|
+
): Promise<
|
|
546
|
+
| { readonly ok: true; readonly value: Uint8Array | null }
|
|
547
|
+
| { readonly ok: false; readonly error: ImportError }
|
|
548
|
+
> {
|
|
549
|
+
if (resolveEncodeMode(compressionMode, { colorSpace, isHdr }) === 'none') {
|
|
550
|
+
return { ok: true, value: null };
|
|
551
|
+
}
|
|
552
|
+
const result = await encodeTextureToKtx2(pixels, width, height, compressionMode, {
|
|
553
|
+
colorSpace,
|
|
554
|
+
isHdr,
|
|
555
|
+
});
|
|
556
|
+
if (!result.ok) {
|
|
557
|
+
return {
|
|
558
|
+
ok: false,
|
|
559
|
+
error: imageConversionFailure(
|
|
560
|
+
ctx,
|
|
561
|
+
'encode',
|
|
562
|
+
result.error.code === 'ktx2-encode-source-too-large'
|
|
563
|
+
? 'ktx2-source-too-large'
|
|
564
|
+
: 'ktx2-encode-refused',
|
|
565
|
+
`${ctx.source}#compressionMode`,
|
|
566
|
+
'the requested compression mode to accept the decoded image',
|
|
567
|
+
`codec:${result.error.code},mode:${result.error.mode}`,
|
|
568
|
+
result.error.code === 'ktx2-encode-source-too-large'
|
|
569
|
+
? 'reduce source dimensions or set compressionMode to none'
|
|
570
|
+
: 'repair the source image or compression settings and retry the import',
|
|
571
|
+
),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return { ok: true, value: result.value.ktx2 };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function importImage(ctx: ImportContext): Promise<ImportResult> {
|
|
578
|
+
const requiredKind = requiredImageOutputKind(ctx.source);
|
|
579
|
+
if (requiredKind !== undefined) {
|
|
580
|
+
const topologyError = validateImageOutputTopology(ctx, requiredKind);
|
|
581
|
+
if (topologyError !== undefined) return { ok: false, error: topologyError };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const read = await ctx.readSource();
|
|
585
|
+
if (!read.ok) {
|
|
586
|
+
return {
|
|
587
|
+
ok: false,
|
|
588
|
+
error: new ImportError({
|
|
589
|
+
code: 'source-read-failed',
|
|
590
|
+
expected: `readable source file at "${ctx.source}"`,
|
|
591
|
+
hint: IMPORT_ERROR_HINTS['source-read-failed'],
|
|
592
|
+
detail: {
|
|
593
|
+
source: ctx.source,
|
|
594
|
+
reason: read.error instanceof Error ? read.error.message : String(read.error),
|
|
595
|
+
},
|
|
596
|
+
}),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
const mime = mimeFromSource(ctx.source);
|
|
600
|
+
|
|
601
|
+
if (ctx.source.toLowerCase().endsWith('.basis')) {
|
|
602
|
+
return importBasisSource(ctx, read.value);
|
|
603
|
+
}
|
|
604
|
+
if (ctx.source.toLowerCase().endsWith('.ktx2')) {
|
|
605
|
+
return importKtx2Source(ctx, read.value);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// --- HDR arm (D-6): .hdr equirect source is decoded via decodeHdr -> f16 ---
|
|
609
|
+
if (mime === undefined && ctx.source.toLowerCase().endsWith('.hdr')) {
|
|
610
|
+
const { decodeHdr } = await import('./hdr-decoder.js');
|
|
611
|
+
const decoded = decodeHdr(read.value);
|
|
612
|
+
if (!decoded.ok) {
|
|
613
|
+
return {
|
|
614
|
+
ok: false,
|
|
615
|
+
error: imageConversionFailure(
|
|
616
|
+
ctx,
|
|
617
|
+
'decode',
|
|
618
|
+
'hdr',
|
|
619
|
+
ctx.source,
|
|
620
|
+
'valid Radiance RGBE bytes to decode into an equirect asset',
|
|
621
|
+
`image:${decoded.error.code}`,
|
|
622
|
+
'repair the HDR header or pixel payload and retry the import',
|
|
623
|
+
),
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const dec = decoded.value;
|
|
627
|
+
const { halfFloat } = await import('@forgeax/engine-math');
|
|
628
|
+
const f16Bytes = halfFloat.f32ToF16Bytes(
|
|
629
|
+
new Uint8Array(dec.data.buffer, dec.data.byteOffset, dec.data.byteLength),
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
// NO block-compression for equirect (feat-20260707 M5 fix). The .hdr arm
|
|
633
|
+
// folds only `kind:'equirect'` sub-assets, and an equirect is ALWAYS an IBL /
|
|
634
|
+
// skybox source: the runtime drives it through equirect-to-cube / irradiance /
|
|
635
|
+
// prefilter / brdf-lut RENDER passes (Skylight.equirect / SkyboxBackground.
|
|
636
|
+
// equirect). A BC6H (block-compressed) texture is sample-only, never a color-
|
|
637
|
+
// renderable render target, so a BC6H equirect breaks cube projection with a
|
|
638
|
+
// "BC6HRGBUfloat is not color renderable" WebGPU error. The equirect must stay
|
|
639
|
+
// uncompressed rgba16float; the catalog `compression` discriminant is forced to
|
|
640
|
+
// 'none' in import-texture.ts (compressionFor), so the two agree. A purely-
|
|
641
|
+
// sampled HDR 2D texture (never folded by this arm) may still take the
|
|
642
|
+
// UASTC-HDR path via the standard image arm below.
|
|
643
|
+
const out: ImportedAsset[] = [];
|
|
644
|
+
for (const sub of ctx.subAssets) {
|
|
645
|
+
// The .hdr arm folds equirect sub-assets only: a single 2D rgba16float
|
|
646
|
+
// image (the lat-long env map) with a disk identity. The cube-to-cube IBL
|
|
647
|
+
// projection is a GPU-side pass driven by the runtime record arm, not a
|
|
648
|
+
// build-time fold (feat-20260630 w5; orchestrator adjudication: equirect
|
|
649
|
+
// produces a build .bin, unlike the retired cube-texture).
|
|
650
|
+
if (sub.kind !== 'equirect') continue;
|
|
651
|
+
const payload: EquirectAsset = {
|
|
652
|
+
kind: 'equirect',
|
|
653
|
+
width: dec.width,
|
|
654
|
+
height: dec.height,
|
|
655
|
+
format: 'rgba16float',
|
|
656
|
+
data: f16Bytes,
|
|
657
|
+
colorSpace: 'linear',
|
|
658
|
+
};
|
|
659
|
+
out.push({
|
|
660
|
+
guid: sub.guid,
|
|
661
|
+
kind: 'equirect',
|
|
662
|
+
payload,
|
|
663
|
+
refs: [],
|
|
664
|
+
artifacts: {
|
|
665
|
+
body: {
|
|
666
|
+
mediaType: 'application/x-forgeax-rgba16f',
|
|
667
|
+
assetCodec: { name: 'rgba16float' },
|
|
668
|
+
bytes: f16Bytes,
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return { ok: true, value: { assets: out, sourceDependencies: [] } };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// --- Standard PNG/JPEG path ---
|
|
677
|
+
if (mime === undefined) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
`imageImporter: unsupported source extension for "${ctx.source}" (expected .png / .jpg / .jpeg / .tga / .hdr / .ktx2 / .basis)`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const colorSpace: ImageColorSpace = ctx.importSettings.colorSpace === 'srgb' ? 'srgb' : 'linear';
|
|
684
|
+
const mipmap = mipmapTokenToBoolean(ctx.importSettings.mipmap);
|
|
685
|
+
const compressionMode = compressionModeToken(ctx.importSettings.compressionMode);
|
|
686
|
+
const downscaleMaxDimension =
|
|
687
|
+
typeof ctx.importSettings.downscaleMaxDimension === 'number' &&
|
|
688
|
+
Number.isInteger(ctx.importSettings.downscaleMaxDimension) &&
|
|
689
|
+
ctx.importSettings.downscaleMaxDimension > 0
|
|
690
|
+
? ctx.importSettings.downscaleMaxDimension
|
|
691
|
+
: undefined;
|
|
692
|
+
|
|
693
|
+
const decoded = parseImage(read.value, mime, {
|
|
694
|
+
colorSpace,
|
|
695
|
+
mipmap,
|
|
696
|
+
...(downscaleMaxDimension !== undefined ? { downscaleMaxDimension } : {}),
|
|
697
|
+
});
|
|
698
|
+
if (!decoded.ok) {
|
|
699
|
+
return {
|
|
700
|
+
ok: false,
|
|
701
|
+
error: imageConversionFailure(
|
|
702
|
+
ctx,
|
|
703
|
+
'decode',
|
|
704
|
+
'ldr',
|
|
705
|
+
ctx.source,
|
|
706
|
+
'valid PNG, JPEG, or TGA bytes to decode into RGBA pixels',
|
|
707
|
+
`image:${decoded.error.code}`,
|
|
708
|
+
'repair the source image bytes and retry the import',
|
|
709
|
+
),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
const dec = decoded.value;
|
|
713
|
+
|
|
714
|
+
// Basis encode arm (M3 w18): null keeps the uncompressed rgba8 `.bin` path.
|
|
715
|
+
const encoded = await maybeEncodeTextureBytes(
|
|
716
|
+
ctx,
|
|
717
|
+
dec.bytes,
|
|
718
|
+
dec.width,
|
|
719
|
+
dec.height,
|
|
720
|
+
compressionMode,
|
|
721
|
+
colorSpace,
|
|
722
|
+
false,
|
|
723
|
+
);
|
|
724
|
+
if (!encoded.ok) return encoded;
|
|
725
|
+
const encodedBytes = encoded.value;
|
|
726
|
+
|
|
727
|
+
const out: ImportedAsset[] = [];
|
|
728
|
+
for (const sub of ctx.subAssets) {
|
|
729
|
+
// Only flat 2D image sub-assets are folded here; cube-texture sub-assets
|
|
730
|
+
// ride the runtime IBL multi-face cook and are intentionally not produced.
|
|
731
|
+
if (sub.kind !== 'texture') continue;
|
|
732
|
+
const payload: TextureAsset = {
|
|
733
|
+
kind: 'texture',
|
|
734
|
+
width: dec.width,
|
|
735
|
+
height: dec.height,
|
|
736
|
+
format: colorSpaceToFormat(colorSpace),
|
|
737
|
+
data: encodedBytes ?? dec.bytes,
|
|
738
|
+
colorSpace,
|
|
739
|
+
mipmap,
|
|
740
|
+
};
|
|
741
|
+
out.push({
|
|
742
|
+
guid: sub.guid,
|
|
743
|
+
kind: 'texture',
|
|
744
|
+
payload,
|
|
745
|
+
refs: [],
|
|
746
|
+
artifacts: {
|
|
747
|
+
body: {
|
|
748
|
+
mediaType: encodedBytes === null ? 'application/x-forgeax-rgba8' : 'image/ktx2',
|
|
749
|
+
assetCodec:
|
|
750
|
+
encodedBytes === null
|
|
751
|
+
? { name: 'rgba8', version: '1' }
|
|
752
|
+
: {
|
|
753
|
+
name: 'basis',
|
|
754
|
+
container: 'ktx2',
|
|
755
|
+
profile: resolveEncodeMode(compressionMode, { colorSpace, isHdr: false }),
|
|
756
|
+
},
|
|
757
|
+
bytes: encodedBytes ?? dec.bytes,
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return { ok: true, value: { assets: out, sourceDependencies: [] } };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* The image {@link Importer}. Register it into an `ImporterRegistry` so the
|
|
767
|
+
* import runner dispatches `meta.importer === 'image'` sidecars here.
|
|
768
|
+
*
|
|
769
|
+
* @example
|
|
770
|
+
* ```ts
|
|
771
|
+
* import { ImporterRegistry } from '@forgeax/engine-import';
|
|
772
|
+
* import { imageImporter } from '@forgeax/engine-image/image-importer';
|
|
773
|
+
* const importers = new ImporterRegistry();
|
|
774
|
+
* importers.register(imageImporter);
|
|
775
|
+
* ```
|
|
776
|
+
*/
|
|
777
|
+
export const imageImporter: Importer = {
|
|
778
|
+
key: 'image',
|
|
779
|
+
import: importImage,
|
|
780
|
+
};
|