@forgeax/engine-image 0.1.19 → 0.1.20
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/README.md +47 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/texture-source-descriptor.integration.test.d.ts +2 -0
- package/dist/__tests__/texture-source-descriptor.integration.test.d.ts.map +1 -0
- package/dist/image-importer.d.ts.map +1 -1
- package/dist/image-importer.mjs +220 -15
- package/dist/image-importer.mjs.map +1 -1
- package/dist/index.mjs +24 -11
- package/dist/index.mjs.map +1 -1
- package/dist/runtime/asset-decoders.d.ts.map +1 -1
- package/dist/texture/importer.d.ts +42 -0
- package/dist/texture/importer.d.ts.map +1 -0
- package/dist/texture/source-descriptor.d.ts +24 -0
- package/dist/texture/source-descriptor.d.ts.map +1 -0
- package/package.json +6 -6
- package/src/__tests__/image-importer-conversion-failure.unit.test.ts +10 -2
- package/src/__tests__/image-importer-topology.unit.test.ts +2 -2
- package/src/__tests__/image.unit.test.ts +2 -4
- package/src/__tests__/ktx2-basis-importer.unit.test.ts +2 -3
- package/src/__tests__/texture-source-descriptor.integration.test.ts +117 -0
- package/src/image-importer.ts +24 -14
- package/src/runtime/asset-decoders.ts +40 -11
- package/src/texture/importer.ts +198 -0
- package/src/texture/source-descriptor.ts +123 -0
package/src/image-importer.ts
CHANGED
|
@@ -46,6 +46,7 @@ import { IMPORT_ERROR_HINTS, ImportError } from '@forgeax/engine-types';
|
|
|
46
46
|
import type { CompressionMode } from './ktx2-encode.js';
|
|
47
47
|
import { encodeTextureToKtx2, resolveEncodeMode } from './ktx2-encode.js';
|
|
48
48
|
import { parseImage } from './parse-image.js';
|
|
49
|
+
import { importTextureSource } from './texture/importer.js';
|
|
49
50
|
|
|
50
51
|
/** Map a source path / mime hint to the parseImage mime literal. */
|
|
51
52
|
function mimeFromSource(source: string): 'image/png' | 'image/jpeg' | undefined {
|
|
@@ -319,13 +320,17 @@ async function importKtx2Source(ctx: ImportContext, bytes: Uint8Array): Promise<
|
|
|
319
320
|
kind: 'texture',
|
|
320
321
|
payload: {
|
|
321
322
|
kind: 'texture',
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
shape: {
|
|
324
|
+
viewDimension: '2d',
|
|
325
|
+
extent: { width: inspection.width, height: inspection.height },
|
|
326
|
+
},
|
|
324
327
|
format: colorSpaceToFormat(inspection.colorSpace),
|
|
325
328
|
data: bytes,
|
|
326
329
|
colorSpace: inspection.colorSpace,
|
|
327
|
-
|
|
328
|
-
|
|
330
|
+
mips:
|
|
331
|
+
inspection.levelCount > 1
|
|
332
|
+
? { kind: 'packed', levelCount: inspection.levelCount }
|
|
333
|
+
: { kind: 'none' },
|
|
329
334
|
},
|
|
330
335
|
refs: [],
|
|
331
336
|
artifacts: {
|
|
@@ -481,13 +486,17 @@ async function importBasisSource(ctx: ImportContext, bytes: Uint8Array): Promise
|
|
|
481
486
|
kind: 'texture',
|
|
482
487
|
payload: {
|
|
483
488
|
kind: 'texture',
|
|
484
|
-
|
|
485
|
-
|
|
489
|
+
shape: {
|
|
490
|
+
viewDimension: '2d',
|
|
491
|
+
extent: { width: inspection.width, height: inspection.height },
|
|
492
|
+
},
|
|
486
493
|
format: colorSpaceToFormat(colorSpace),
|
|
487
494
|
data: bytes,
|
|
488
495
|
colorSpace,
|
|
489
|
-
|
|
490
|
-
|
|
496
|
+
mips:
|
|
497
|
+
inspection.levelCount > 1
|
|
498
|
+
? { kind: 'packed', levelCount: inspection.levelCount }
|
|
499
|
+
: { kind: 'none' },
|
|
491
500
|
},
|
|
492
501
|
refs: [],
|
|
493
502
|
artifacts: {
|
|
@@ -574,6 +583,9 @@ async function maybeEncodeTextureBytes(
|
|
|
574
583
|
}
|
|
575
584
|
|
|
576
585
|
async function importImage(ctx: ImportContext): Promise<ImportResult> {
|
|
586
|
+
if (ctx.source.toLowerCase().endsWith('.texture.json')) {
|
|
587
|
+
return importTextureSource(ctx);
|
|
588
|
+
}
|
|
577
589
|
const requiredKind = requiredImageOutputKind(ctx.source);
|
|
578
590
|
if (requiredKind !== undefined) {
|
|
579
591
|
const topologyError = validateImageOutputTopology(ctx, requiredKind);
|
|
@@ -730,12 +742,11 @@ async function importImage(ctx: ImportContext): Promise<ImportResult> {
|
|
|
730
742
|
if (sub.kind !== 'texture') continue;
|
|
731
743
|
const payload: TextureAsset = {
|
|
732
744
|
kind: 'texture',
|
|
733
|
-
width: dec.width,
|
|
734
|
-
height: dec.height,
|
|
745
|
+
shape: { viewDimension: '2d', extent: { width: dec.width, height: dec.height } },
|
|
735
746
|
format: colorSpaceToFormat(colorSpace),
|
|
736
747
|
data: encodedBytes ?? dec.bytes,
|
|
737
748
|
colorSpace,
|
|
738
|
-
mipmap,
|
|
749
|
+
mips: mipmap ? { kind: 'generate' } : { kind: 'none' },
|
|
739
750
|
};
|
|
740
751
|
out.push({
|
|
741
752
|
guid: sub.guid,
|
|
@@ -825,11 +836,10 @@ export const decodeImageForImport: ImportContext['decodeImage'] = async (
|
|
|
825
836
|
texture: {
|
|
826
837
|
kind: 'texture' as const,
|
|
827
838
|
data: cookedBytes,
|
|
828
|
-
width: tex.width,
|
|
829
|
-
height: tex.height,
|
|
839
|
+
shape: { viewDimension: '2d', extent: { width: tex.width, height: tex.height } },
|
|
830
840
|
format: colorSpaceToFormat(colorSpace),
|
|
831
841
|
colorSpace,
|
|
832
|
-
mipmap,
|
|
842
|
+
mips: mipmap ? { kind: 'generate' as const } : { kind: 'none' as const },
|
|
833
843
|
},
|
|
834
844
|
bytes: cookedBytes,
|
|
835
845
|
mediaType,
|
|
@@ -37,11 +37,32 @@ function compressedImageTarget(colorSpace: TextureAsset['colorSpace']): TextureA
|
|
|
37
37
|
return colorSpace === 'srgb' ? 'rgba8unorm-srgb' : 'rgba8unorm';
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function
|
|
40
|
+
function validTextureSurface(
|
|
41
41
|
value: unknown,
|
|
42
|
-
): value is Pick<TextureAsset, '
|
|
42
|
+
): value is Pick<TextureAsset, 'shape' | 'format' | 'data' | 'colorSpace' | 'mips'> {
|
|
43
43
|
if (value === null || typeof value !== 'object') return false;
|
|
44
44
|
const candidate = value as Partial<TextureAsset>;
|
|
45
|
+
const shape = candidate.shape;
|
|
46
|
+
const extent = shape?.viewDimension === '2d' ? shape.extent : undefined;
|
|
47
|
+
const mips = candidate.mips;
|
|
48
|
+
return (
|
|
49
|
+
extent !== undefined &&
|
|
50
|
+
validDimensions(extent.width, extent.height) &&
|
|
51
|
+
mips !== undefined &&
|
|
52
|
+
(mips.kind === 'none' ||
|
|
53
|
+
mips.kind === 'generate' ||
|
|
54
|
+
(mips.kind === 'packed' && Number.isSafeInteger(mips.levelCount) && mips.levelCount > 0)) &&
|
|
55
|
+
typeof candidate.format === 'string' &&
|
|
56
|
+
imageBytes(candidate.data) !== undefined &&
|
|
57
|
+
(candidate.colorSpace === 'srgb' || candidate.colorSpace === 'linear')
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validEquirectSurface(
|
|
62
|
+
value: unknown,
|
|
63
|
+
): value is Pick<EquirectAsset, 'width' | 'height' | 'format' | 'data' | 'colorSpace'> {
|
|
64
|
+
if (value === null || typeof value !== 'object') return false;
|
|
65
|
+
const candidate = value as Partial<EquirectAsset>;
|
|
45
66
|
return (
|
|
46
67
|
validDimensions(candidate.width ?? 0, candidate.height ?? 0) &&
|
|
47
68
|
typeof candidate.format === 'string' &&
|
|
@@ -96,17 +117,24 @@ async function readImageSurface<P extends TextureAsset | EquirectAsset>(
|
|
|
96
117
|
const mip = transcoded.value.mips[0];
|
|
97
118
|
if (mip === undefined) return invalid(envelope.guid, expected, 'codec:base-mip-missing');
|
|
98
119
|
data = mip.data;
|
|
99
|
-
return readDecodedSurface(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
return readDecodedSurface(
|
|
121
|
+
envelope.guid,
|
|
122
|
+
expected,
|
|
123
|
+
kind,
|
|
124
|
+
kind === 'texture'
|
|
125
|
+
? {
|
|
126
|
+
...payload,
|
|
127
|
+
shape: { viewDimension: '2d', extent: { width: mip.width, height: mip.height } },
|
|
128
|
+
format: target,
|
|
129
|
+
data,
|
|
130
|
+
mips: { kind: 'none' },
|
|
131
|
+
}
|
|
132
|
+
: { ...payload, width: mip.width, height: mip.height, format: target, data },
|
|
133
|
+
);
|
|
106
134
|
}
|
|
107
135
|
}
|
|
108
136
|
|
|
109
|
-
return readDecodedSurface(envelope.guid, expected, {
|
|
137
|
+
return readDecodedSurface(envelope.guid, expected, kind, {
|
|
110
138
|
...payload,
|
|
111
139
|
...(data === undefined ? {} : { data }),
|
|
112
140
|
});
|
|
@@ -115,9 +143,10 @@ async function readImageSurface<P extends TextureAsset | EquirectAsset>(
|
|
|
115
143
|
function readDecodedSurface<P extends TextureAsset | EquirectAsset>(
|
|
116
144
|
guid: string,
|
|
117
145
|
expected: string,
|
|
146
|
+
kind: P['kind'],
|
|
118
147
|
candidate: unknown,
|
|
119
148
|
): ReturnType<typeof ok<P>> | ReturnType<typeof err<AssetLoadError>> {
|
|
120
|
-
if (!
|
|
149
|
+
if (kind === 'texture' ? !validTextureSurface(candidate) : !validEquirectSurface(candidate)) {
|
|
121
150
|
return invalid(guid, expected, 'image owner validation failed');
|
|
122
151
|
}
|
|
123
152
|
return ok(candidate as P);
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ImportContext,
|
|
3
|
+
ImportedAsset,
|
|
4
|
+
TextureAsset,
|
|
5
|
+
TextureError,
|
|
6
|
+
} from '@forgeax/engine-types';
|
|
7
|
+
import { deriveTextureLayout, err, ImportError, ok, type Result } from '@forgeax/engine-types';
|
|
8
|
+
import {
|
|
9
|
+
parseTextureSourceDescriptor,
|
|
10
|
+
type TextureSourceDescriptor,
|
|
11
|
+
type TextureSourceDescriptorError,
|
|
12
|
+
} from './source-descriptor.js';
|
|
13
|
+
|
|
14
|
+
export interface TextureSourceInput {
|
|
15
|
+
readonly descriptor: unknown;
|
|
16
|
+
readonly guid: string;
|
|
17
|
+
readonly sourceKey: string;
|
|
18
|
+
readSibling(
|
|
19
|
+
uri: string,
|
|
20
|
+
): Promise<
|
|
21
|
+
| { readonly ok: true; readonly value: Uint8Array }
|
|
22
|
+
| { readonly ok: false; readonly error: unknown }
|
|
23
|
+
>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TextureSourceReadError {
|
|
27
|
+
readonly code: 'source-read-failed';
|
|
28
|
+
readonly expected: string;
|
|
29
|
+
readonly hint: string;
|
|
30
|
+
readonly detail: {
|
|
31
|
+
readonly sourceKey: string;
|
|
32
|
+
readonly sibling: string;
|
|
33
|
+
readonly reason: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type TextureSourceError =
|
|
38
|
+
| TextureSourceDescriptorError
|
|
39
|
+
| TextureSourceReadError
|
|
40
|
+
| TextureError;
|
|
41
|
+
|
|
42
|
+
export type TextureSourceResult = Result<ImportedAsset<TextureAsset>, TextureSourceError>;
|
|
43
|
+
|
|
44
|
+
function sourceReadError(input: TextureSourceInput, reason: unknown): TextureSourceReadError {
|
|
45
|
+
return {
|
|
46
|
+
code: 'source-read-failed',
|
|
47
|
+
expected: `readable raw sibling "${input.descriptor && typeof input.descriptor === 'object' && 'rawSibling' in input.descriptor ? input.descriptor.rawSibling : 'rawSibling'}"`,
|
|
48
|
+
hint: 'repair the raw sibling path or bytes and re-import the same texture GUID',
|
|
49
|
+
detail: {
|
|
50
|
+
sourceKey: input.sourceKey,
|
|
51
|
+
sibling:
|
|
52
|
+
input.descriptor && typeof input.descriptor === 'object' && 'rawSibling' in input.descriptor
|
|
53
|
+
? String(input.descriptor.rawSibling)
|
|
54
|
+
: 'rawSibling',
|
|
55
|
+
reason: reason instanceof Error ? reason.message : String(reason),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function bodyMediaType(format: GPUTextureFormat): string {
|
|
61
|
+
return format === 'r8unorm' ? 'application/x-forgeax-r8' : `application/x-forgeax-${format}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Produce one canonical TextureAsset and body artifact from a descriptor sibling. */
|
|
65
|
+
export async function produceTextureSource(
|
|
66
|
+
input: TextureSourceInput,
|
|
67
|
+
): Promise<TextureSourceResult> {
|
|
68
|
+
const descriptorResult = parseTextureSourceDescriptor(input.descriptor);
|
|
69
|
+
if (!descriptorResult.ok) return descriptorResult;
|
|
70
|
+
const descriptor = descriptorResult.value;
|
|
71
|
+
const sibling = await input.readSibling(descriptor.rawSibling);
|
|
72
|
+
if (!sibling.ok) return err(sourceReadError(input, sibling.error));
|
|
73
|
+
|
|
74
|
+
const layout = deriveTextureLayout({
|
|
75
|
+
shape: descriptor.shape,
|
|
76
|
+
format: descriptor.format,
|
|
77
|
+
mips: descriptor.mips,
|
|
78
|
+
actualByteLength: sibling.value.byteLength,
|
|
79
|
+
order: 'mip-major,image-major,row-major',
|
|
80
|
+
});
|
|
81
|
+
if (!layout.ok) return layout;
|
|
82
|
+
|
|
83
|
+
const data = new Uint8Array(sibling.value);
|
|
84
|
+
return ok({
|
|
85
|
+
guid: input.guid,
|
|
86
|
+
kind: 'texture',
|
|
87
|
+
payload: {
|
|
88
|
+
kind: 'texture',
|
|
89
|
+
shape: descriptor.shape,
|
|
90
|
+
format: descriptor.format,
|
|
91
|
+
colorSpace: descriptor.colorSpace,
|
|
92
|
+
mips: descriptor.mips,
|
|
93
|
+
data,
|
|
94
|
+
},
|
|
95
|
+
refs: [],
|
|
96
|
+
artifacts: {
|
|
97
|
+
body: {
|
|
98
|
+
mediaType: bodyMediaType(descriptor.format),
|
|
99
|
+
assetCodec: { name: descriptor.format, version: '1' },
|
|
100
|
+
bytes: data,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sourceValidationError(ctx: ImportContext, error: TextureSourceError): ImportError {
|
|
107
|
+
const detail = 'detail' in error ? error.detail : { field: 'descriptor', actual: error };
|
|
108
|
+
return new ImportError({
|
|
109
|
+
code: 'source-validation-failed',
|
|
110
|
+
expected: error.expected,
|
|
111
|
+
hint: error.hint,
|
|
112
|
+
detail: {
|
|
113
|
+
diagnostics: [
|
|
114
|
+
{
|
|
115
|
+
code: `texture-source-${error.code}`,
|
|
116
|
+
severity: 'error',
|
|
117
|
+
sourcePath: `${ctx.source}#${'field' in detail ? detail.field : 'rawSibling'}`,
|
|
118
|
+
sourceRange: { start: 0, end: 0, line: 1, column: 1 },
|
|
119
|
+
rule: 'texture-source-descriptor',
|
|
120
|
+
expected: error.expected,
|
|
121
|
+
actual: JSON.stringify(detail),
|
|
122
|
+
hint: error.hint,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Import a JSON descriptor whose canonical bytes live in one raw sibling. */
|
|
130
|
+
export async function importTextureSource(ctx: ImportContext): Promise<
|
|
131
|
+
| {
|
|
132
|
+
readonly ok: true;
|
|
133
|
+
readonly value: {
|
|
134
|
+
readonly assets: readonly ImportedAsset[];
|
|
135
|
+
readonly sourceDependencies: readonly string[];
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
| { readonly ok: false; readonly error: ImportError }
|
|
139
|
+
> {
|
|
140
|
+
const source = await ctx.readSource();
|
|
141
|
+
if (!source.ok) {
|
|
142
|
+
const reason = String(source.error);
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: new ImportError({
|
|
146
|
+
code: 'source-read-failed',
|
|
147
|
+
expected: `readable texture descriptor at "${ctx.source}"`,
|
|
148
|
+
hint: 'repair the texture descriptor path and retry the import',
|
|
149
|
+
detail: { source: ctx.source, reason },
|
|
150
|
+
}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let descriptor: unknown;
|
|
155
|
+
try {
|
|
156
|
+
descriptor = JSON.parse(new TextDecoder().decode(source.value));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: sourceValidationError(ctx, {
|
|
161
|
+
code: 'texture-source-descriptor-invalid',
|
|
162
|
+
expected: 'JSON texture source descriptor',
|
|
163
|
+
hint: 'repair the descriptor JSON and retry the import',
|
|
164
|
+
detail: {
|
|
165
|
+
field: 'descriptor',
|
|
166
|
+
actual: error instanceof Error ? error.message : String(error),
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const subAsset = ctx.subAssets.length === 1 ? ctx.subAssets[0] : undefined;
|
|
172
|
+
if (subAsset === undefined || subAsset.kind !== 'texture' || subAsset.sourceIndex !== 0) {
|
|
173
|
+
return {
|
|
174
|
+
ok: false,
|
|
175
|
+
error: sourceValidationError(ctx, {
|
|
176
|
+
code: 'texture-source-descriptor-invalid',
|
|
177
|
+
expected: 'one texture subAsset at sourceIndex 0',
|
|
178
|
+
hint: 'repair Meta subAssets and retry the same texture GUID',
|
|
179
|
+
detail: { field: 'subAssets', actual: ctx.subAssets },
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const produced = await produceTextureSource({
|
|
184
|
+
descriptor,
|
|
185
|
+
guid: subAsset.guid,
|
|
186
|
+
sourceKey: subAsset.sourceKey ?? `${ctx.source}:texture`,
|
|
187
|
+
readSibling: ctx.readSibling,
|
|
188
|
+
});
|
|
189
|
+
if (!produced.ok) return { ok: false, error: sourceValidationError(ctx, produced.error) };
|
|
190
|
+
const parsed = parseTextureSourceDescriptor(descriptor);
|
|
191
|
+
const sibling = parsed.ok ? parsed.value.rawSibling : ctx.source;
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
value: { assets: [produced.value], sourceDependencies: [ctx.source, sibling] },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type { TextureSourceDescriptor };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { TextureMipPolicy, TextureShape } from '@forgeax/engine-types';
|
|
2
|
+
import {
|
|
3
|
+
err,
|
|
4
|
+
ok,
|
|
5
|
+
type Result,
|
|
6
|
+
type TextureError,
|
|
7
|
+
validateTextureShape,
|
|
8
|
+
} from '@forgeax/engine-types';
|
|
9
|
+
|
|
10
|
+
/** Versioned source-side declaration for one logical sampled texture. */
|
|
11
|
+
export interface TextureSourceDescriptor {
|
|
12
|
+
readonly schemaVersion: '1';
|
|
13
|
+
readonly shape: TextureShape;
|
|
14
|
+
readonly format: GPUTextureFormat;
|
|
15
|
+
readonly colorSpace: 'srgb' | 'linear';
|
|
16
|
+
readonly mips: TextureMipPolicy;
|
|
17
|
+
readonly rawSibling: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TextureSourceDescriptorError {
|
|
21
|
+
readonly code: 'texture-source-descriptor-invalid';
|
|
22
|
+
readonly expected: string;
|
|
23
|
+
readonly hint: string;
|
|
24
|
+
readonly detail: { readonly field: string; readonly actual: unknown };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type TextureSourceDescriptorResult = Result<
|
|
28
|
+
TextureSourceDescriptor,
|
|
29
|
+
TextureSourceDescriptorError | TextureError
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
33
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function positiveInteger(value: unknown): value is number {
|
|
37
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseShape(value: unknown): TextureShape | undefined {
|
|
41
|
+
if (!record(value) || !record(value.extent)) return undefined;
|
|
42
|
+
const extent = value.extent;
|
|
43
|
+
if (
|
|
44
|
+
value.viewDimension === '2d' &&
|
|
45
|
+
positiveInteger(extent.width) &&
|
|
46
|
+
positiveInteger(extent.height)
|
|
47
|
+
) {
|
|
48
|
+
if (extent.layers === undefined && extent.depth === undefined) {
|
|
49
|
+
return { viewDimension: '2d', extent: { width: extent.width, height: extent.height } };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
value.viewDimension === '2d-array' &&
|
|
54
|
+
positiveInteger(extent.width) &&
|
|
55
|
+
positiveInteger(extent.height) &&
|
|
56
|
+
positiveInteger(extent.layers) &&
|
|
57
|
+
extent.depth === undefined
|
|
58
|
+
) {
|
|
59
|
+
return {
|
|
60
|
+
viewDimension: '2d-array',
|
|
61
|
+
extent: { width: extent.width, height: extent.height, layers: extent.layers },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
value.viewDimension === '3d' &&
|
|
66
|
+
positiveInteger(extent.width) &&
|
|
67
|
+
positiveInteger(extent.height) &&
|
|
68
|
+
positiveInteger(extent.depth) &&
|
|
69
|
+
extent.layers === undefined
|
|
70
|
+
) {
|
|
71
|
+
return {
|
|
72
|
+
viewDimension: '3d',
|
|
73
|
+
extent: { width: extent.width, height: extent.height, depth: extent.depth },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseMips(value: unknown): TextureMipPolicy | undefined {
|
|
80
|
+
if (!record(value)) return undefined;
|
|
81
|
+
if (value.kind === 'none' || value.kind === 'generate') return { kind: value.kind };
|
|
82
|
+
if (value.kind === 'packed' && positiveInteger(value.levelCount)) {
|
|
83
|
+
return { kind: 'packed', levelCount: value.levelCount };
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function descriptorError(field: string, actual: unknown): TextureSourceDescriptorError {
|
|
89
|
+
return {
|
|
90
|
+
code: 'texture-source-descriptor-invalid',
|
|
91
|
+
expected: 'version 1 descriptor with shape, format, colorSpace, mips, and rawSibling',
|
|
92
|
+
hint: 'repair the texture descriptor and re-import the same texture GUID',
|
|
93
|
+
detail: { field, actual },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Parse and validate source facts before any raw sibling bytes are consumed. */
|
|
98
|
+
export function parseTextureSourceDescriptor(value: unknown): TextureSourceDescriptorResult {
|
|
99
|
+
if (!record(value)) return err(descriptorError('descriptor', value));
|
|
100
|
+
if (value.schemaVersion !== '1')
|
|
101
|
+
return err(descriptorError('schemaVersion', value.schemaVersion));
|
|
102
|
+
const shape = parseShape(value.shape);
|
|
103
|
+
if (shape === undefined) return err(descriptorError('shape', value.shape));
|
|
104
|
+
const mips = parseMips(value.mips);
|
|
105
|
+
if (mips === undefined) return err(descriptorError('mips', value.mips));
|
|
106
|
+
if (typeof value.format !== 'string') return err(descriptorError('format', value.format));
|
|
107
|
+
if (value.colorSpace !== 'srgb' && value.colorSpace !== 'linear') {
|
|
108
|
+
return err(descriptorError('colorSpace', value.colorSpace));
|
|
109
|
+
}
|
|
110
|
+
if (typeof value.rawSibling !== 'string' || value.rawSibling.trim().length === 0) {
|
|
111
|
+
return err(descriptorError('rawSibling', value.rawSibling));
|
|
112
|
+
}
|
|
113
|
+
const shapeResult = validateTextureShape(shape, mips, value.format as GPUTextureFormat);
|
|
114
|
+
if (!shapeResult.ok) return shapeResult;
|
|
115
|
+
return ok({
|
|
116
|
+
schemaVersion: '1',
|
|
117
|
+
shape,
|
|
118
|
+
format: value.format as GPUTextureFormat,
|
|
119
|
+
colorSpace: value.colorSpace,
|
|
120
|
+
mips,
|
|
121
|
+
rawSibling: value.rawSibling,
|
|
122
|
+
});
|
|
123
|
+
}
|