@forgeax/engine-gltf 0.1.21 → 0.1.24
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 +115 -3
- package/dist/__tests__/lod-import.integration.test.d.ts +2 -0
- package/dist/__tests__/lod-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/lod-meta-publication.integration.test.d.ts +2 -0
- package/dist/__tests__/lod-meta-publication.integration.test.d.ts.map +1 -0
- package/dist/__tests__/physical-clearcoat.unit.test.d.ts +2 -0
- package/dist/__tests__/physical-clearcoat.unit.test.d.ts.map +1 -0
- package/dist/__tests__/physical-material-import.integration.test.d.ts +2 -0
- package/dist/__tests__/physical-material-import.integration.test.d.ts.map +1 -0
- package/dist/bridge.d.ts +1 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/check-extensions.d.ts +1 -1
- package/dist/check-extensions.d.ts.map +1 -1
- package/dist/cli-gltf.mjs +362 -14
- package/dist/cli-gltf.mjs.map +1 -1
- package/dist/errors.d.ts +13 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/gltf-importer.d.ts.map +1 -1
- package/dist/image-color-space.d.ts +12 -0
- package/dist/image-color-space.d.ts.map +1 -1
- package/dist/importer-entry.mjs +516 -25
- package/dist/importer-entry.mjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +609 -28
- package/dist/index.mjs.map +1 -1
- package/dist/lod/parse-lod.d.ts +15 -0
- package/dist/lod/parse-lod.d.ts.map +1 -0
- package/dist/lod/project-meta.d.ts +23 -0
- package/dist/lod/project-meta.d.ts.map +1 -0
- package/dist/material/parse-material.d.ts +66 -0
- package/dist/material/parse-material.d.ts.map +1 -1
- package/dist/node-file-entry.mjs +265 -8
- package/dist/node-file-entry.mjs.map +1 -1
- package/dist/parse-gltf.d.ts +3 -1
- package/dist/parse-gltf.d.ts.map +1 -1
- package/dist/reimport-reuse-meta.d.ts +1 -1
- package/dist/reimport-reuse-meta.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/__tests__/bridge-material-values.unit.test.ts +84 -1
- package/src/__tests__/bridge-skin-stride.unit.test.ts +22 -0
- package/src/__tests__/fixtures/lod/core-only.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod-malformed.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod-multi-root.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod-node-extras.gltf +1 -0
- package/src/__tests__/fixtures/lod/msft-lod.gltf +1 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +8 -0
- package/src/__tests__/gltf-error-migration.test.ts +3 -3
- package/src/__tests__/gltf.unit.test.ts +60 -5
- package/src/__tests__/lod-import.integration.test.ts +106 -0
- package/src/__tests__/lod-meta-publication.integration.test.ts +24 -0
- package/src/__tests__/material-pack-refs.integration.test.ts +53 -0
- package/src/__tests__/parse-gltf.unit.test.ts +50 -0
- package/src/__tests__/physical-clearcoat.unit.test.ts +110 -0
- package/src/__tests__/physical-material-import.integration.test.ts +239 -0
- package/src/__tests__/source-key-producer.integration.test.ts +6 -2
- package/src/bridge.ts +259 -14
- package/src/check-extensions.ts +16 -8
- package/src/errors.ts +32 -2
- package/src/gltf-importer.ts +122 -1
- package/src/image-color-space.ts +29 -4
- package/src/index.ts +10 -1
- package/src/lod/parse-lod.ts +129 -0
- package/src/lod/project-meta.ts +48 -0
- package/src/material/parse-material.ts +307 -0
- package/src/parse-gltf.ts +97 -1
- package/src/reimport-reuse-meta.ts +3 -1
- package/dist/.tsbuildinfo +0 -1
|
@@ -162,6 +162,56 @@ describe('glTF transmission material IR', () => {
|
|
|
162
162
|
});
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
describe('glTF specular material IR', () => {
|
|
166
|
+
it('projects specularTexture through the alpha channel', () => {
|
|
167
|
+
const result = parseMaterialIr(
|
|
168
|
+
{
|
|
169
|
+
extensions: {
|
|
170
|
+
KHR_materials_specular: {
|
|
171
|
+
specularFactor: 0.75,
|
|
172
|
+
specularTexture: { index: 0 },
|
|
173
|
+
specularColorFactor: [0.8, 0.7, 0.6],
|
|
174
|
+
specularColorTexture: { index: 1 },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
[{ source: 0 }, { source: 1 }],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
expect(result.ok).toBe(true);
|
|
182
|
+
if (!result.ok) return;
|
|
183
|
+
expect(result.value).toMatchObject({
|
|
184
|
+
specularFactor: 0.75,
|
|
185
|
+
specularChannel: 3,
|
|
186
|
+
specularTexture: { texture: 0 },
|
|
187
|
+
specularColorFactor: [0.8, 0.7, 0.6],
|
|
188
|
+
specularColorChannel: [0, 1, 2],
|
|
189
|
+
specularColorTexture: { texture: 1 },
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('glTF required extension admission', () => {
|
|
195
|
+
it.each([
|
|
196
|
+
'KHR_materials_transmission',
|
|
197
|
+
'KHR_materials_ior',
|
|
198
|
+
'KHR_materials_volume',
|
|
199
|
+
])('accepts %s through the parse path', async (extension) => {
|
|
200
|
+
const result = await parseGltf(
|
|
201
|
+
{
|
|
202
|
+
asset: { version: '2.0' },
|
|
203
|
+
extensionsUsed: [extension],
|
|
204
|
+
extensionsRequired: [extension],
|
|
205
|
+
},
|
|
206
|
+
noopLoader,
|
|
207
|
+
`/required-${extension}.gltf`,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(result.ok).toBe(true);
|
|
211
|
+
if (!result.ok) return;
|
|
212
|
+
expect(result.value.diagnostics.unsupportedExtensions).toEqual([]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
165
215
|
describe('glTF COLOR_0 importer carrier', () => {
|
|
166
216
|
it('publishes normalized VEC3 FLOAT colors as importer-owned RGBA', async () => {
|
|
167
217
|
const fixture = JSON.parse(
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateMaterialTangentInputs } from '../bridge.js';
|
|
3
|
+
import { checkExtensions } from '../check-extensions.js';
|
|
4
|
+
import { parseMaterial } from '../material/parse-material.js';
|
|
5
|
+
|
|
6
|
+
describe('KHR_materials_clearcoat projection', () => {
|
|
7
|
+
it('projects factor, R/G textures, coat normal RG, scale, and transform facts', () => {
|
|
8
|
+
const result = parseMaterial(
|
|
9
|
+
{
|
|
10
|
+
extensions: {
|
|
11
|
+
KHR_materials_clearcoat: {
|
|
12
|
+
clearcoatFactor: 0.75,
|
|
13
|
+
clearcoatTexture: {
|
|
14
|
+
index: 0,
|
|
15
|
+
texCoord: 2,
|
|
16
|
+
extensions: { KHR_texture_transform: { offset: [0.25, 0.5] } },
|
|
17
|
+
},
|
|
18
|
+
clearcoatRoughnessFactor: 0.25,
|
|
19
|
+
clearcoatRoughnessTexture: { index: 1, texCoord: 3 },
|
|
20
|
+
clearcoatNormalTexture: { index: 2, texCoord: 4, scale: 0.5 },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
} as never,
|
|
24
|
+
[{ source: 0 }, { source: 1 }, { source: 2 }],
|
|
25
|
+
);
|
|
26
|
+
expect(result.ok).toBe(true);
|
|
27
|
+
if (result.ok) {
|
|
28
|
+
expect(result.value).toMatchObject({
|
|
29
|
+
clearcoatFactor: 0.75,
|
|
30
|
+
clearcoatChannel: 0,
|
|
31
|
+
clearcoatRoughnessFactor: 0.25,
|
|
32
|
+
clearcoatRoughnessChannel: 1,
|
|
33
|
+
clearcoatNormalTexture: { texture: 2, texCoord: 4, scale: 0.5 },
|
|
34
|
+
clearcoatNormalChannel: [0, 1],
|
|
35
|
+
});
|
|
36
|
+
expect((result.value as unknown as Record<string, unknown>).clearcoatTexture).toMatchObject({
|
|
37
|
+
texture: 0,
|
|
38
|
+
texCoord: 2,
|
|
39
|
+
transform: { offset: [0.25, 0.5] },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('admits the clearcoat extension as a required extension', () => {
|
|
45
|
+
const result = checkExtensions({ extensionsRequired: ['KHR_materials_clearcoat'] });
|
|
46
|
+
expect(result.ok).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('reports tangent admission with material, mesh, layer, UV, and attributes', () => {
|
|
50
|
+
const result = validateMaterialTangentInputs(
|
|
51
|
+
{
|
|
52
|
+
name: 'coat',
|
|
53
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
54
|
+
metallicFactor: 0,
|
|
55
|
+
roughnessFactor: 0.5,
|
|
56
|
+
clearcoatNormalTexture: { texture: 0 },
|
|
57
|
+
} as never,
|
|
58
|
+
{
|
|
59
|
+
name: 'mesh',
|
|
60
|
+
positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
|
|
61
|
+
texcoord0: new Float32Array([0, 0, 0, 0, 0, 0]),
|
|
62
|
+
materialIndex: 0,
|
|
63
|
+
meshIndex: 0,
|
|
64
|
+
indices: new Uint32Array([0, 1, 2]),
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
expect(result.ok).toBe(false);
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
expect(result.error.code).toBe('material-tangent-required');
|
|
70
|
+
expect(result.error.detail).toMatchObject({
|
|
71
|
+
material: 'coat',
|
|
72
|
+
mesh: 'mesh',
|
|
73
|
+
layer: 'clearcoat',
|
|
74
|
+
uv: 'TEXCOORD_0',
|
|
75
|
+
attributes: ['NORMAL', 'TEXCOORD_0', 'TANGENT'],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('requires a tangent frame for scalar-only anisotropy', () => {
|
|
81
|
+
const result = validateMaterialTangentInputs(
|
|
82
|
+
{
|
|
83
|
+
name: 'anisotropic-scalar',
|
|
84
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
85
|
+
metallicFactor: 0,
|
|
86
|
+
roughnessFactor: 0.5,
|
|
87
|
+
anisotropyStrength: 0.4,
|
|
88
|
+
anisotropyRotation: 0,
|
|
89
|
+
} as never,
|
|
90
|
+
{
|
|
91
|
+
name: 'mesh-without-frame',
|
|
92
|
+
positions: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
|
|
93
|
+
materialIndex: 0,
|
|
94
|
+
meshIndex: 0,
|
|
95
|
+
indices: new Uint32Array([0, 1, 2]),
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
expect(result.ok).toBe(false);
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
expect(result.error.code).toBe('material-tangent-required');
|
|
101
|
+
expect(result.error.detail).toMatchObject({
|
|
102
|
+
material: 'anisotropic-scalar',
|
|
103
|
+
mesh: 'mesh-without-frame',
|
|
104
|
+
layer: 'anisotropy',
|
|
105
|
+
uv: 'TEXCOORD_0',
|
|
106
|
+
attributes: ['NORMAL', 'TEXCOORD_0', 'TANGENT'],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
2
|
+
import {
|
|
3
|
+
ImporterRegistry,
|
|
4
|
+
type ImportRunnerFs,
|
|
5
|
+
type RunImportMeta,
|
|
6
|
+
runImport,
|
|
7
|
+
} from '@forgeax/engine-import';
|
|
8
|
+
import type { TextureAsset } from '@forgeax/engine-types';
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { gltfImporter } from '../gltf-importer.js';
|
|
11
|
+
|
|
12
|
+
const SOURCE = 'physical-material.gltf';
|
|
13
|
+
const TINY_PNG_BASE64 =
|
|
14
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4//8/AAX+Av4N70a4AAAAAElFTkSuQmCC';
|
|
15
|
+
|
|
16
|
+
const MATERIAL_GUID = '019f0000-0000-7000-8000-000000000100';
|
|
17
|
+
const TEXTURE_GUIDS = [
|
|
18
|
+
'019f0000-0000-7000-8000-000000000101',
|
|
19
|
+
'019f0000-0000-7000-8000-000000000102',
|
|
20
|
+
'019f0000-0000-7000-8000-000000000103',
|
|
21
|
+
'019f0000-0000-7000-8000-000000000104',
|
|
22
|
+
'019f0000-0000-7000-8000-000000000105',
|
|
23
|
+
] as const;
|
|
24
|
+
const SAMPLER_GUIDS = [
|
|
25
|
+
'019f0000-0000-7000-8000-000000000106',
|
|
26
|
+
'019f0000-0000-7000-8000-000000000107',
|
|
27
|
+
'019f0000-0000-7000-8000-000000000108',
|
|
28
|
+
'019f0000-0000-7000-8000-000000000109',
|
|
29
|
+
'019f0000-0000-7000-8000-00000000010a',
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
function dataUri(): string {
|
|
33
|
+
return `data:image/png;base64,${TINY_PNG_BASE64}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sourceBytes(): Uint8Array {
|
|
37
|
+
const textures = TEXTURE_GUIDS.map((_guid, index) => ({ source: index, sampler: index }));
|
|
38
|
+
return new TextEncoder().encode(
|
|
39
|
+
JSON.stringify({
|
|
40
|
+
asset: { version: '2.0' },
|
|
41
|
+
extensionsUsed: [
|
|
42
|
+
'KHR_materials_clearcoat',
|
|
43
|
+
'KHR_materials_anisotropy',
|
|
44
|
+
'KHR_materials_sheen',
|
|
45
|
+
'KHR_materials_iridescence',
|
|
46
|
+
'KHR_materials_specular',
|
|
47
|
+
],
|
|
48
|
+
materials: [
|
|
49
|
+
{
|
|
50
|
+
name: 'PhysicalSurface',
|
|
51
|
+
pbrMetallicRoughness: {
|
|
52
|
+
baseColorFactor: [0.8, 0.7, 0.6, 1],
|
|
53
|
+
metallicFactor: 0.1,
|
|
54
|
+
roughnessFactor: 0.35,
|
|
55
|
+
},
|
|
56
|
+
extensions: {
|
|
57
|
+
KHR_materials_clearcoat: {
|
|
58
|
+
clearcoatFactor: 0.75,
|
|
59
|
+
clearcoatRoughnessFactor: 0.2,
|
|
60
|
+
clearcoatNormalTexture: { index: 0 },
|
|
61
|
+
},
|
|
62
|
+
KHR_materials_anisotropy: {
|
|
63
|
+
anisotropyStrength: 0.5,
|
|
64
|
+
anisotropyRotation: 0.25,
|
|
65
|
+
anisotropyTexture: { index: 1 },
|
|
66
|
+
},
|
|
67
|
+
KHR_materials_sheen: {
|
|
68
|
+
sheenColorFactor: [0.2, 0.1, 0.05],
|
|
69
|
+
sheenRoughnessFactor: 0.3,
|
|
70
|
+
sheenColorTexture: { index: 2 },
|
|
71
|
+
},
|
|
72
|
+
KHR_materials_iridescence: {
|
|
73
|
+
iridescenceFactor: 0.6,
|
|
74
|
+
iridescenceIor: 1.4,
|
|
75
|
+
iridescenceThicknessMinimum: 120,
|
|
76
|
+
iridescenceThicknessMaximum: 380,
|
|
77
|
+
iridescenceTexture: { index: 3 },
|
|
78
|
+
},
|
|
79
|
+
KHR_materials_specular: {
|
|
80
|
+
specularFactor: 0.8,
|
|
81
|
+
specularColorFactor: [0.9, 0.85, 0.75],
|
|
82
|
+
specularColorTexture: { index: 4 },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
textures,
|
|
88
|
+
samplers: [{}, {}, {}, {}, {}],
|
|
89
|
+
images: TEXTURE_GUIDS.map(() => ({ uri: dataUri(), mimeType: 'image/png' })),
|
|
90
|
+
scenes: [],
|
|
91
|
+
nodes: [],
|
|
92
|
+
meshes: [],
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function meta(): RunImportMeta {
|
|
98
|
+
return {
|
|
99
|
+
importer: 'gltf',
|
|
100
|
+
source: SOURCE,
|
|
101
|
+
subAssets: [
|
|
102
|
+
{
|
|
103
|
+
guid: MATERIAL_GUID,
|
|
104
|
+
sourceIndex: 0,
|
|
105
|
+
sourceKey: 'material/physical-surface',
|
|
106
|
+
kind: 'material',
|
|
107
|
+
},
|
|
108
|
+
...TEXTURE_GUIDS.map((guid, sourceIndex) => ({
|
|
109
|
+
guid,
|
|
110
|
+
sourceIndex,
|
|
111
|
+
sourceKey: `texture/${sourceIndex}`,
|
|
112
|
+
kind: 'texture' as const,
|
|
113
|
+
})),
|
|
114
|
+
...SAMPLER_GUIDS.map((guid, sourceIndex) => ({
|
|
115
|
+
guid,
|
|
116
|
+
sourceIndex,
|
|
117
|
+
sourceKey: `sampler/${sourceIndex}`,
|
|
118
|
+
kind: 'sampler' as const,
|
|
119
|
+
})),
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function decodeBase64(value: string): Uint8Array {
|
|
125
|
+
const binary = atob(value);
|
|
126
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function fs(): ImportRunnerFs {
|
|
130
|
+
return {
|
|
131
|
+
readSource: async (sourcePath) =>
|
|
132
|
+
sourcePath === SOURCE
|
|
133
|
+
? { ok: true, value: sourceBytes() }
|
|
134
|
+
: { ok: false, error: new Error(`unexpected source path ${sourcePath}`) },
|
|
135
|
+
decodeImage: async (_bytes, _mimeType, settings) => {
|
|
136
|
+
const colorSpace = settings.colorSpace === 'srgb' ? 'srgb' : 'linear';
|
|
137
|
+
const texture: TextureAsset = {
|
|
138
|
+
kind: 'texture',
|
|
139
|
+
shape: { viewDimension: '2d', extent: { width: 1, height: 1 } },
|
|
140
|
+
format: colorSpace === 'srgb' ? 'rgba8unorm-srgb' : 'rgba8unorm',
|
|
141
|
+
data: new Uint8Array([128, 96, 64, 255]),
|
|
142
|
+
colorSpace,
|
|
143
|
+
mips: { kind: 'generate' },
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
value: {
|
|
148
|
+
texture,
|
|
149
|
+
bytes: decodeBase64(TINY_PNG_BASE64),
|
|
150
|
+
mediaType: 'image/png',
|
|
151
|
+
assetCodec: { name: 'rgba8', version: '1' },
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
describe('glTF physical material importer route', () => {
|
|
159
|
+
it('carries all five KHR layers from importer through pack and runtime material parsing', async () => {
|
|
160
|
+
const importers = new ImporterRegistry();
|
|
161
|
+
importers.register(gltfImporter);
|
|
162
|
+
|
|
163
|
+
const result = await runImport(meta(), importers, fs());
|
|
164
|
+
expect(result.ok).toBe(true);
|
|
165
|
+
if (!result.ok || 'skipped' in result.value)
|
|
166
|
+
throw new Error('physical import did not publish a pack');
|
|
167
|
+
|
|
168
|
+
expect(result.value.pack.assets).toHaveLength(11);
|
|
169
|
+
expect(result.value.cookProducts).toHaveLength(11);
|
|
170
|
+
expect(result.value.cookProducts.every((product) => product.receipt !== undefined)).toBe(true);
|
|
171
|
+
|
|
172
|
+
const material = result.value.pack.assets.find((asset) => asset.guid === MATERIAL_GUID);
|
|
173
|
+
if (material === undefined) throw new Error('physical material pack row missing');
|
|
174
|
+
expect(material.refs).toEqual([
|
|
175
|
+
TEXTURE_GUIDS[0],
|
|
176
|
+
SAMPLER_GUIDS[0],
|
|
177
|
+
TEXTURE_GUIDS[1],
|
|
178
|
+
SAMPLER_GUIDS[1],
|
|
179
|
+
TEXTURE_GUIDS[2],
|
|
180
|
+
SAMPLER_GUIDS[2],
|
|
181
|
+
TEXTURE_GUIDS[3],
|
|
182
|
+
SAMPLER_GUIDS[3],
|
|
183
|
+
TEXTURE_GUIDS[4],
|
|
184
|
+
SAMPLER_GUIDS[4],
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const payload = material.payload;
|
|
188
|
+
const parameterNames = new Set(
|
|
189
|
+
(payload.parameters as readonly { name: string }[]).map((parameter) => parameter.name),
|
|
190
|
+
);
|
|
191
|
+
for (const name of [
|
|
192
|
+
'clearcoat',
|
|
193
|
+
'anisotropyStrength',
|
|
194
|
+
'sheenColor',
|
|
195
|
+
'iridescence',
|
|
196
|
+
'specular',
|
|
197
|
+
'clearcoatNormalTexture',
|
|
198
|
+
'anisotropyTexture',
|
|
199
|
+
'sheenColorTexture',
|
|
200
|
+
'iridescenceTexture',
|
|
201
|
+
'specularColorTexture',
|
|
202
|
+
]) {
|
|
203
|
+
expect(parameterNames.has(name)).toBe(true);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const runtime = new AssetRegistry({
|
|
207
|
+
findMaterialArtifact: () => ({ ok: false, error: new Error('shader registry not wired') }),
|
|
208
|
+
} as never);
|
|
209
|
+
const parsed = runtime.parseAndReturnAsset({
|
|
210
|
+
kind: material.kind,
|
|
211
|
+
payload,
|
|
212
|
+
refs: [...material.refs],
|
|
213
|
+
});
|
|
214
|
+
expect(parsed.ok).toBe(true);
|
|
215
|
+
if (!parsed.ok) return;
|
|
216
|
+
expect(parsed.value.asset.kind).toBe('material');
|
|
217
|
+
const values = (parsed.value.asset as { values?: Record<string, unknown> }).values ?? {};
|
|
218
|
+
expect(values.clearcoatNormalTexture).toMatchObject({
|
|
219
|
+
texture: TEXTURE_GUIDS[0],
|
|
220
|
+
sampler: SAMPLER_GUIDS[0],
|
|
221
|
+
});
|
|
222
|
+
expect(values.anisotropyTexture).toMatchObject({
|
|
223
|
+
texture: TEXTURE_GUIDS[1],
|
|
224
|
+
sampler: SAMPLER_GUIDS[1],
|
|
225
|
+
});
|
|
226
|
+
expect(values.sheenColorTexture).toMatchObject({
|
|
227
|
+
texture: TEXTURE_GUIDS[2],
|
|
228
|
+
sampler: SAMPLER_GUIDS[2],
|
|
229
|
+
});
|
|
230
|
+
expect(values.iridescenceTexture).toMatchObject({
|
|
231
|
+
texture: TEXTURE_GUIDS[3],
|
|
232
|
+
sampler: SAMPLER_GUIDS[3],
|
|
233
|
+
});
|
|
234
|
+
expect(values.specularColorTexture).toMatchObject({
|
|
235
|
+
texture: TEXTURE_GUIDS[4],
|
|
236
|
+
sampler: SAMPLER_GUIDS[4],
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
@@ -86,7 +86,9 @@ describe('glTF producer source-key boundary', () => {
|
|
|
86
86
|
expect(result.ok).toBe(false);
|
|
87
87
|
if (result.ok) return;
|
|
88
88
|
expect(result.error.code).toBe('duplicate-source-key');
|
|
89
|
-
expect(result.error.detail.sourceIndices).toEqual([
|
|
89
|
+
expect((result.error.detail as { sourceIndices: readonly number[] }).sourceIndices).toEqual([
|
|
90
|
+
0, 1,
|
|
91
|
+
]);
|
|
90
92
|
});
|
|
91
93
|
|
|
92
94
|
it('applies the same preflight to duplicate material names', () => {
|
|
@@ -95,7 +97,9 @@ describe('glTF producer source-key boundary', () => {
|
|
|
95
97
|
expect(result.ok).toBe(false);
|
|
96
98
|
if (result.ok) return;
|
|
97
99
|
expect(result.error.code).toBe('duplicate-source-key');
|
|
98
|
-
expect(result.error.detail.sourceIndices).toEqual([
|
|
100
|
+
expect((result.error.detail as { sourceIndices: readonly number[] }).sourceIndices).toEqual([
|
|
101
|
+
0, 1,
|
|
102
|
+
]);
|
|
99
103
|
});
|
|
100
104
|
|
|
101
105
|
it('publishes producer-valid output when semantic identity is unique', () => {
|