@forgeax/engine-gltf 0.0.0-dev.8d955ade1c79
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 +204 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-target-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-samplers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-material-values.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-mixed-color.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts +2 -0
- package/dist/__tests__/bridge-sampler-required.test-d.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-shader-route.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge-skin-stride.unit.test.d.ts.map +1 -0
- package/dist/__tests__/bridge-test-helpers.d.ts +5 -0
- package/dist/__tests__/bridge-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/bridge.unit.test.d.ts +2 -0
- package/dist/__tests__/bridge.unit.test.d.ts.map +1 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts +2 -0
- package/dist/__tests__/cli-gltf.integration.test.d.ts.map +1 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts +2 -0
- package/dist/__tests__/color-0-accessor.unit.test.d.ts.map +1 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/data-uri-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts +2 -0
- package/dist/__tests__/extension-admission.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts +2 -0
- package/dist/__tests__/gltf-error-derived-views.test-d.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-migration.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf-error-policy-owner.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-importer.test.d.ts +2 -0
- package/dist/__tests__/gltf-importer.test.d.ts.map +1 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/gltf-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts +2 -0
- package/dist/__tests__/malformed-buffer-data-uri.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-missing-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts +2 -0
- package/dist/__tests__/material-pack-refs.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts +2 -0
- package/dist/__tests__/material-per-slot-ir.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-transform.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts +2 -0
- package/dist/__tests__/material-unused-uv.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts +2 -0
- package/dist/__tests__/meshopt-falsifier.unit.test.d.ts.map +1 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts +2 -0
- package/dist/__tests__/meshopt-mode-filter-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts +2 -0
- package/dist/__tests__/meshopt-real-matrix.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.integration.test.d.ts +2 -0
- package/dist/__tests__/morph-import.integration.test.d.ts.map +1 -0
- package/dist/__tests__/morph-import.unit.test.d.ts +2 -0
- package/dist/__tests__/morph-import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-decode.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-multi-uv.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf-skin-attrs.unit.test.d.ts.map +1 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts +2 -0
- package/dist/__tests__/parse-gltf.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts +2 -0
- package/dist/__tests__/pick-e2e.integration.test.d.ts.map +1 -0
- package/dist/__tests__/public-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/public-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-diagnostics.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-key-producer.integration.test.d.ts +2 -0
- package/dist/__tests__/source-key-producer.integration.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/accessor/decode-accessor.d.ts +75 -0
- package/dist/accessor/decode-accessor.d.ts.map +1 -0
- package/dist/accessor/decode-color.d.ts +15 -0
- package/dist/accessor/decode-color.d.ts.map +1 -0
- package/dist/accessor/index.d.ts +3 -0
- package/dist/accessor/index.d.ts.map +1 -0
- package/dist/bridge.d.ts +103 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/check-extensions.d.ts +29 -0
- package/dist/check-extensions.d.ts.map +1 -0
- package/dist/cli-gltf.d.ts +8 -0
- package/dist/cli-gltf.d.ts.map +1 -0
- package/dist/cli-gltf.mjs +2435 -0
- package/dist/cli-gltf.mjs.map +1 -0
- package/dist/data-uri.d.ts +6 -0
- package/dist/data-uri.d.ts.map +1 -0
- package/dist/errors.d.ts +201 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/extract-image-bytes.d.ts +42 -0
- package/dist/extract-image-bytes.d.ts.map +1 -0
- package/dist/gltf-importer.d.ts +21 -0
- package/dist/gltf-importer.d.ts.map +1 -0
- package/dist/image-color-space.d.ts +35 -0
- package/dist/image-color-space.d.ts.map +1 -0
- package/dist/importer-entry.d.ts +10 -0
- package/dist/importer-entry.d.ts.map +1 -0
- package/dist/importer-entry.mjs +3378 -0
- package/dist/importer-entry.mjs.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3673 -0
- package/dist/index.mjs.map +1 -0
- package/dist/meshopt-decode.d.ts +39 -0
- package/dist/meshopt-decode.d.ts.map +1 -0
- package/dist/node-file-entry.d.ts +14 -0
- package/dist/node-file-entry.d.ts.map +1 -0
- package/dist/node-file-entry.mjs +1946 -0
- package/dist/node-file-entry.mjs.map +1 -0
- package/dist/node-path.d.ts +14 -0
- package/dist/node-path.d.ts.map +1 -0
- package/dist/parse-animation.d.ts +70 -0
- package/dist/parse-animation.d.ts.map +1 -0
- package/dist/parse-glb-chunks.d.ts +25 -0
- package/dist/parse-glb-chunks.d.ts.map +1 -0
- package/dist/parse-glb.d.ts +3 -0
- package/dist/parse-glb.d.ts.map +1 -0
- package/dist/parse-gltf-header.d.ts +18 -0
- package/dist/parse-gltf-header.d.ts.map +1 -0
- package/dist/parse-gltf.d.ts +263 -0
- package/dist/parse-gltf.d.ts.map +1 -0
- package/dist/parse-skin.d.ts +70 -0
- package/dist/parse-skin.d.ts.map +1 -0
- package/dist/reimport-reuse-meta.d.ts +48 -0
- package/dist/reimport-reuse-meta.d.ts.map +1 -0
- package/dist/serialize-meta.d.ts +2 -0
- package/dist/serialize-meta.d.ts.map +1 -0
- package/dist/source-key.d.ts +44 -0
- package/dist/source-key.d.ts.map +1 -0
- package/dist/sub-asset-key.d.ts +12 -0
- package/dist/sub-asset-key.d.ts.map +1 -0
- package/dist/to-asset-pack.d.ts +2 -0
- package/dist/to-asset-pack.d.ts.map +1 -0
- package/dist/transform.d.ts +37 -0
- package/dist/transform.d.ts.map +1 -0
- package/package.json +92 -0
- package/src/__tests__/animation-target-errors.unit.test.ts +51 -0
- package/src/__tests__/bridge-material-samplers.unit.test.ts +67 -0
- package/src/__tests__/bridge-material-values.unit.test.ts +106 -0
- package/src/__tests__/bridge-mixed-color.unit.test.ts +100 -0
- package/src/__tests__/bridge-sampler-required.test-d.ts +59 -0
- package/src/__tests__/bridge-skin-shader-route.unit.test.ts +42 -0
- package/src/__tests__/bridge-skin-stride.unit.test.ts +115 -0
- package/src/__tests__/bridge-test-helpers.ts +9 -0
- package/src/__tests__/bridge.unit.test.ts +1227 -0
- package/src/__tests__/cli-gltf.integration.test.ts +122 -0
- package/src/__tests__/color-0-accessor.unit.test.ts +174 -0
- package/src/__tests__/data-uri-surface.unit.test.ts +26 -0
- package/src/__tests__/extension-admission.unit.test.ts +19 -0
- package/src/__tests__/fixtures/color-0/float-vec3.gltf +13 -0
- package/src/__tests__/fixtures/color-0/interleaved-vec3.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ubyte.gltf +6 -0
- package/src/__tests__/fixtures/color-0/normalized-ushort.gltf +6 -0
- package/src/__tests__/gltf-error-derived-views.test-d.ts +152 -0
- package/src/__tests__/gltf-error-migration.test.ts +147 -0
- package/src/__tests__/gltf-error-policy-owner.unit.test.ts +185 -0
- package/src/__tests__/gltf-importer.test.ts +12 -0
- package/src/__tests__/gltf-local-artifacts.test.ts +11 -0
- package/src/__tests__/gltf.unit.test.ts +2975 -0
- package/src/__tests__/malformed-buffer-data-uri.integration.test.ts +140 -0
- package/src/__tests__/material-missing-uv.unit.test.ts +29 -0
- package/src/__tests__/material-pack-refs.integration.test.ts +58 -0
- package/src/__tests__/material-per-slot-ir.unit.test.ts +90 -0
- package/src/__tests__/material-texture-transform.unit.test.ts +105 -0
- package/src/__tests__/material-unused-uv.unit.test.ts +44 -0
- package/src/__tests__/meshopt-decode.unit.test.ts +87 -0
- package/src/__tests__/meshopt-falsifier.unit.test.ts +92 -0
- package/src/__tests__/meshopt-mode-filter-owner.test-d.ts +79 -0
- package/src/__tests__/meshopt-real-matrix.integration.test.ts +370 -0
- package/src/__tests__/morph-import.integration.test.ts +115 -0
- package/src/__tests__/morph-import.unit.test.ts +62 -0
- package/src/__tests__/parse-decode.unit.test.ts +919 -0
- package/src/__tests__/parse-gltf-multi-uv.test.ts +238 -0
- package/src/__tests__/parse-gltf-skin-attrs.unit.test.ts +191 -0
- package/src/__tests__/parse-gltf.unit.test.ts +133 -0
- package/src/__tests__/pick-e2e.integration.test.ts +238 -0
- package/src/__tests__/public-surface.unit.test.ts +10 -0
- package/src/__tests__/source-key-diagnostics.integration.test.ts +140 -0
- package/src/__tests__/source-key-producer.integration.test.ts +171 -0
- package/src/__tests__/source-key.unit.test.ts +159 -0
- package/src/accessor/decode-accessor.ts +309 -0
- package/src/accessor/decode-color.ts +128 -0
- package/src/accessor/index.ts +15 -0
- package/src/bridge.ts +928 -0
- package/src/check-extensions.ts +85 -0
- package/src/cli-gltf.ts +339 -0
- package/src/data-uri.ts +29 -0
- package/src/errors.ts +386 -0
- package/src/extract-image-bytes.ts +343 -0
- package/src/gltf-importer.ts +976 -0
- package/src/image-color-space.ts +104 -0
- package/src/importer-entry.ts +24 -0
- package/src/index.ts +148 -0
- package/src/meshopt-decode.ts +200 -0
- package/src/node-file-entry.ts +98 -0
- package/src/node-path.ts +41 -0
- package/src/parse-animation.ts +286 -0
- package/src/parse-glb-chunks.ts +167 -0
- package/src/parse-glb.ts +16 -0
- package/src/parse-gltf-header.ts +58 -0
- package/src/parse-gltf.ts +1731 -0
- package/src/parse-skin.ts +238 -0
- package/src/reimport-reuse-meta.ts +134 -0
- package/src/serialize-meta.ts +24 -0
- package/src/source-key.ts +106 -0
- package/src/sub-asset-key.ts +61 -0
- package/src/to-asset-pack.ts +8 -0
- package/src/transform.ts +117 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ImporterRegistry,
|
|
3
|
+
type ImportRunnerFs,
|
|
4
|
+
type RunImportMeta,
|
|
5
|
+
runImport,
|
|
6
|
+
} from '@forgeax/engine-import';
|
|
7
|
+
import type { ImportContext } from '@forgeax/engine-types';
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { extractImageBytes } from '../extract-image-bytes.js';
|
|
10
|
+
import { gltfImporter } from '../gltf-importer.js';
|
|
11
|
+
import { parseGltfForImporter } from '../parse-gltf.js';
|
|
12
|
+
|
|
13
|
+
const SOURCE = 'malformed-buffer.gltf';
|
|
14
|
+
const MESH_GUID = '019f0000-0000-7000-8000-000000000088';
|
|
15
|
+
|
|
16
|
+
const meta: RunImportMeta = {
|
|
17
|
+
importer: 'gltf',
|
|
18
|
+
source: SOURCE,
|
|
19
|
+
subAssets: [{ guid: MESH_GUID, sourceIndex: 0, sourceKey: 'mesh', kind: 'mesh' }],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function dataUri(bytes: Uint8Array): string {
|
|
23
|
+
return `data:application/octet-stream;base64,${btoa(
|
|
24
|
+
Array.from(bytes, (byte) => String.fromCharCode(byte)).join(''),
|
|
25
|
+
)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sourceBytes(bufferUri: string): Uint8Array {
|
|
29
|
+
return new TextEncoder().encode(
|
|
30
|
+
JSON.stringify({
|
|
31
|
+
asset: { version: '2.0' },
|
|
32
|
+
buffers: [{ uri: bufferUri, byteLength: 36 }],
|
|
33
|
+
bufferViews: [{ buffer: 0, byteOffset: 0, byteLength: 36 }],
|
|
34
|
+
accessors: [{ bufferView: 0, componentType: 5126, count: 3, type: 'VEC3' }],
|
|
35
|
+
meshes: [{ primitives: [{ attributes: { POSITION: 0 } }] }],
|
|
36
|
+
nodes: [],
|
|
37
|
+
scenes: [],
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sourceFs(initialSource: Uint8Array): {
|
|
43
|
+
readonly fs: ImportRunnerFs;
|
|
44
|
+
setSource(source: Uint8Array): void;
|
|
45
|
+
} {
|
|
46
|
+
let source = initialSource;
|
|
47
|
+
return {
|
|
48
|
+
fs: {
|
|
49
|
+
readSource: async (sourcePath) =>
|
|
50
|
+
sourcePath === SOURCE
|
|
51
|
+
? { ok: true as const, value: source }
|
|
52
|
+
: { ok: false as const, error: new Error(`unexpected source path ${sourcePath}`) },
|
|
53
|
+
},
|
|
54
|
+
setSource(nextSource) {
|
|
55
|
+
source = nextSource;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('malformed glTF buffer data URI recovery', () => {
|
|
61
|
+
it('returns structured refusal, then retries the same registry and GUIDs exactly', async () => {
|
|
62
|
+
const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
|
|
63
|
+
const validSource = sourceBytes(dataUri(new Uint8Array(positions.buffer)));
|
|
64
|
+
const malformedSource = sourceBytes('data:application/octet-stream;base64,%%%%');
|
|
65
|
+
const source = sourceFs(malformedSource);
|
|
66
|
+
const registry = new ImporterRegistry();
|
|
67
|
+
registry.register(gltfImporter);
|
|
68
|
+
|
|
69
|
+
const parsed = await parseGltfForImporter(
|
|
70
|
+
JSON.parse(new TextDecoder().decode(malformedSource)) as object,
|
|
71
|
+
async () => new ArrayBuffer(0),
|
|
72
|
+
SOURCE,
|
|
73
|
+
);
|
|
74
|
+
expect(parsed.ok).toBe(false);
|
|
75
|
+
if (!parsed.ok) expect(parsed.error.code).toBe('source-validation-failed');
|
|
76
|
+
|
|
77
|
+
const failed = await runImport(meta, registry, source.fs);
|
|
78
|
+
expect(failed.ok).toBe(false);
|
|
79
|
+
expect(failed).not.toHaveProperty('value');
|
|
80
|
+
if (!failed.ok) {
|
|
81
|
+
expect(failed.error.code).toBe('source-validation-failed');
|
|
82
|
+
expect(failed.error.detail).toMatchObject({
|
|
83
|
+
diagnostics: [
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
code: 'gltf-buffer-data-uri-invalid',
|
|
86
|
+
sourcePath: SOURCE,
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
source.setSource(validSource);
|
|
93
|
+
const repaired = await runImport(meta, registry, source.fs);
|
|
94
|
+
expect(repaired.ok).toBe(true);
|
|
95
|
+
if (!repaired.ok || 'skipped' in repaired.value)
|
|
96
|
+
throw new Error('repaired import must publish a Pack');
|
|
97
|
+
expect(repaired.value.pack.assets).toHaveLength(1);
|
|
98
|
+
expect(repaired.value.cookProducts).toHaveLength(1);
|
|
99
|
+
|
|
100
|
+
const retried = await runImport(meta, registry, source.fs);
|
|
101
|
+
expect(retried.ok).toBe(true);
|
|
102
|
+
if (!retried.ok || 'skipped' in retried.value)
|
|
103
|
+
throw new Error('same-registry retry must publish a Pack');
|
|
104
|
+
expect(retried.value.pack).toEqual(repaired.value.pack);
|
|
105
|
+
expect(retried.value.cookProducts).toEqual(repaired.value.cookProducts);
|
|
106
|
+
expect(registry.registeredImporters()).toEqual(['gltf']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('turns a malformed bufferView image data URI into an extraction failure', async () => {
|
|
110
|
+
const source = new TextEncoder().encode(
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
asset: { version: '2.0' },
|
|
113
|
+
buffers: [{ uri: 'data:application/octet-stream;base64,%%%%', byteLength: 4 }],
|
|
114
|
+
bufferViews: [{ buffer: 0, byteOffset: 0, byteLength: 4 }],
|
|
115
|
+
images: [{ bufferView: 0, mimeType: 'image/png' }],
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
const context: ImportContext = {
|
|
119
|
+
source: 'image-buffer.gltf',
|
|
120
|
+
readSource: async () => ({ ok: true as const, value: source }),
|
|
121
|
+
readSibling: async () => ({ ok: true as const, value: new Uint8Array() }),
|
|
122
|
+
decodeImage: async () => {
|
|
123
|
+
throw new Error('decodeImage should not run for a failed extraction');
|
|
124
|
+
},
|
|
125
|
+
subAssets: [],
|
|
126
|
+
importSettings: {},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const result = await extractImageBytes(source, context.source, context);
|
|
130
|
+
|
|
131
|
+
expect(result.extracted.size).toBe(0);
|
|
132
|
+
expect(result.failures).toEqual([
|
|
133
|
+
{
|
|
134
|
+
imageIndex: 0,
|
|
135
|
+
source: 'bufferView',
|
|
136
|
+
reason: expect.stringContaining('buffer 0 data URI base64 decode failed'),
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { validateMaterialUvSets } from '../bridge.js';
|
|
3
|
+
import type { GltfMaterialIr } from '../parse-gltf.js';
|
|
4
|
+
|
|
5
|
+
describe('glTF material UV validation', () => {
|
|
6
|
+
it('returns structured failure without a material product when a requested set is absent', () => {
|
|
7
|
+
const result = validateMaterialUvSets(
|
|
8
|
+
{
|
|
9
|
+
name: 'MissingUvMaterial',
|
|
10
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
11
|
+
metallicFactor: 1,
|
|
12
|
+
roughnessFactor: 1,
|
|
13
|
+
baseColorTexture: { texture: 0, texCoord: 2 },
|
|
14
|
+
} as unknown as GltfMaterialIr,
|
|
15
|
+
'primitive-3',
|
|
16
|
+
[0, 1],
|
|
17
|
+
);
|
|
18
|
+
expect(result.ok).toBe(false);
|
|
19
|
+
if (result.ok) return;
|
|
20
|
+
expect(result.error.code).toBe('gltf-material-uv-set-missing');
|
|
21
|
+
expect(result.error.detail).toEqual({
|
|
22
|
+
material: 'MissingUvMaterial',
|
|
23
|
+
primitive: 'primitive-3',
|
|
24
|
+
slot: 'baseColorTexture',
|
|
25
|
+
requestedSet: 2,
|
|
26
|
+
availableSets: [0, 1],
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { materialRefsForPack } from '../gltf-importer.js';
|
|
3
|
+
import type { GltfDoc, GltfMaterialIr } from '../parse-gltf.js';
|
|
4
|
+
|
|
5
|
+
describe('glTF material Pack refs', () => {
|
|
6
|
+
it('emits texture and sampler dependencies for every material slot', () => {
|
|
7
|
+
const material = {
|
|
8
|
+
name: 'PackMaterial',
|
|
9
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
10
|
+
metallicFactor: 1,
|
|
11
|
+
roughnessFactor: 1,
|
|
12
|
+
baseColorTexture: { texture: 0, sampler: 0 },
|
|
13
|
+
metallicRoughnessTexture: { texture: 1, sampler: 1 },
|
|
14
|
+
normalTexture: { texture: 2, sampler: 2 },
|
|
15
|
+
occlusionTexture: { texture: 3, sampler: 3 },
|
|
16
|
+
emissiveTexture: { texture: 4, sampler: 4 },
|
|
17
|
+
} as unknown as GltfMaterialIr;
|
|
18
|
+
const doc = {
|
|
19
|
+
textures: [
|
|
20
|
+
{ source: 0, sampler: 0 },
|
|
21
|
+
{ source: 1, sampler: 1 },
|
|
22
|
+
{ source: 2, sampler: 2 },
|
|
23
|
+
{ source: 3, sampler: 3 },
|
|
24
|
+
{ source: 4, sampler: 4 },
|
|
25
|
+
],
|
|
26
|
+
} as unknown as GltfDoc;
|
|
27
|
+
const refs = materialRefsForPack(
|
|
28
|
+
material,
|
|
29
|
+
doc,
|
|
30
|
+
new Map([
|
|
31
|
+
[0, 'texture-0'],
|
|
32
|
+
[1, 'texture-1'],
|
|
33
|
+
[2, 'texture-2'],
|
|
34
|
+
[3, 'texture-3'],
|
|
35
|
+
[4, 'texture-4'],
|
|
36
|
+
]),
|
|
37
|
+
new Map([
|
|
38
|
+
[0, 'sampler-0'],
|
|
39
|
+
[1, 'sampler-1'],
|
|
40
|
+
[2, 'sampler-2'],
|
|
41
|
+
[3, 'sampler-3'],
|
|
42
|
+
[4, 'sampler-4'],
|
|
43
|
+
]),
|
|
44
|
+
);
|
|
45
|
+
expect(refs.map((ref) => ref.guid)).toEqual([
|
|
46
|
+
'texture-0',
|
|
47
|
+
'sampler-0',
|
|
48
|
+
'texture-1',
|
|
49
|
+
'sampler-1',
|
|
50
|
+
'texture-2',
|
|
51
|
+
'sampler-2',
|
|
52
|
+
'texture-3',
|
|
53
|
+
'sampler-3',
|
|
54
|
+
'texture-4',
|
|
55
|
+
'sampler-4',
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseGltf } from '../parse-gltf.js';
|
|
3
|
+
|
|
4
|
+
const noopLoader = async (_uri: string) => new ArrayBuffer(0);
|
|
5
|
+
|
|
6
|
+
describe('glTF per-slot material IR', () => {
|
|
7
|
+
it('keeps texture, sampler, coordinates, transform, and slot scalars independent', async () => {
|
|
8
|
+
const result = await parseGltf(
|
|
9
|
+
{
|
|
10
|
+
asset: { version: '2.0' },
|
|
11
|
+
textures: [
|
|
12
|
+
{ source: 0, sampler: 0 },
|
|
13
|
+
{ source: 1, sampler: 1 },
|
|
14
|
+
{ source: 2, sampler: 2 },
|
|
15
|
+
{ source: 3, sampler: 3 },
|
|
16
|
+
{ source: 4, sampler: 4 },
|
|
17
|
+
],
|
|
18
|
+
materials: [
|
|
19
|
+
{
|
|
20
|
+
pbrMetallicRoughness: {
|
|
21
|
+
baseColorTexture: {
|
|
22
|
+
index: 0,
|
|
23
|
+
texCoord: 1,
|
|
24
|
+
extensions: {
|
|
25
|
+
KHR_texture_transform: {
|
|
26
|
+
offset: [0.1, 0.2],
|
|
27
|
+
rotation: 0.3,
|
|
28
|
+
scale: [1.1, 1.2],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
metallicRoughnessTexture: { index: 1, texCoord: 2 },
|
|
33
|
+
},
|
|
34
|
+
normalTexture: {
|
|
35
|
+
index: 2,
|
|
36
|
+
texCoord: 3,
|
|
37
|
+
scale: 0.7,
|
|
38
|
+
extensions: {
|
|
39
|
+
KHR_texture_transform: {
|
|
40
|
+
offset: [0.4, 0.5],
|
|
41
|
+
rotation: 0.6,
|
|
42
|
+
scale: [1.4, 1.5],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
occlusionTexture: { index: 3, texCoord: 4, strength: 0.8 },
|
|
47
|
+
emissiveTexture: { index: 4, texCoord: 5 },
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
noopLoader,
|
|
52
|
+
'/per-slot.gltf',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(result.ok).toBe(true);
|
|
56
|
+
if (!result.ok) return;
|
|
57
|
+
|
|
58
|
+
const material = result.value.materials[0] as unknown as Record<string, unknown>;
|
|
59
|
+
expect(material.baseColorTexture).toEqual({
|
|
60
|
+
texture: 0,
|
|
61
|
+
sampler: 0,
|
|
62
|
+
texCoord: 1,
|
|
63
|
+
transform: { offset: [0.1, 0.2], rotation: 0.3, scale: [1.1, 1.2] },
|
|
64
|
+
});
|
|
65
|
+
expect(material.metallicRoughnessTexture).toEqual({
|
|
66
|
+
texture: 1,
|
|
67
|
+
sampler: 1,
|
|
68
|
+
texCoord: 2,
|
|
69
|
+
});
|
|
70
|
+
expect(material.normalTexture).toEqual({
|
|
71
|
+
texture: 2,
|
|
72
|
+
sampler: 2,
|
|
73
|
+
texCoord: 3,
|
|
74
|
+
transform: { offset: [0.4, 0.5], rotation: 0.6, scale: [1.4, 1.5] },
|
|
75
|
+
scale: 0.7,
|
|
76
|
+
});
|
|
77
|
+
expect(material.occlusionTexture).toEqual({
|
|
78
|
+
texture: 3,
|
|
79
|
+
sampler: 3,
|
|
80
|
+
texCoord: 4,
|
|
81
|
+
strength: 0.8,
|
|
82
|
+
});
|
|
83
|
+
expect(material.emissiveTexture).toEqual({
|
|
84
|
+
texture: 4,
|
|
85
|
+
sampler: 4,
|
|
86
|
+
texCoord: 5,
|
|
87
|
+
});
|
|
88
|
+
expect(material.coordinatesSet).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Handle } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { toMaterialAsset } from '../bridge.js';
|
|
4
|
+
import { parseGltf } from '../parse-gltf.js';
|
|
5
|
+
|
|
6
|
+
const noopLoader = async (_uri: string) => new ArrayBuffer(0);
|
|
7
|
+
|
|
8
|
+
describe('glTF KHR_texture_transform per-slot parsing', () => {
|
|
9
|
+
it('keeps transform and texCoord override attached to each texture slot', async () => {
|
|
10
|
+
const result = await parseGltf(
|
|
11
|
+
{
|
|
12
|
+
asset: { version: '2.0' },
|
|
13
|
+
extensionsRequired: ['KHR_texture_transform'],
|
|
14
|
+
extensionsUsed: ['KHR_texture_transform'],
|
|
15
|
+
textures: [
|
|
16
|
+
{ source: 0, sampler: 0 },
|
|
17
|
+
{ source: 1, sampler: 1 },
|
|
18
|
+
],
|
|
19
|
+
materials: [
|
|
20
|
+
{
|
|
21
|
+
pbrMetallicRoughness: {
|
|
22
|
+
baseColorTexture: {
|
|
23
|
+
index: 0,
|
|
24
|
+
texCoord: 0,
|
|
25
|
+
extensions: {
|
|
26
|
+
KHR_texture_transform: {
|
|
27
|
+
texCoord: 6,
|
|
28
|
+
offset: [0.25, 0.5],
|
|
29
|
+
rotation: 0.75,
|
|
30
|
+
scale: [2, 3],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
metallicRoughnessTexture: {
|
|
35
|
+
index: 1,
|
|
36
|
+
texCoord: 1,
|
|
37
|
+
extensions: {
|
|
38
|
+
KHR_texture_transform: {
|
|
39
|
+
texCoord: 7,
|
|
40
|
+
offset: [-0.25, -0.5],
|
|
41
|
+
rotation: -0.75,
|
|
42
|
+
scale: [4, 5],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
noopLoader,
|
|
51
|
+
'/texture-transform.gltf',
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(result.ok).toBe(true);
|
|
55
|
+
if (!result.ok) return;
|
|
56
|
+
const material = result.value.materials[0] as unknown as Record<
|
|
57
|
+
string,
|
|
58
|
+
{ texCoord?: number; transform?: Record<string, unknown> } | undefined
|
|
59
|
+
>;
|
|
60
|
+
if (
|
|
61
|
+
material.baseColorTexture === undefined ||
|
|
62
|
+
material.metallicRoughnessTexture === undefined
|
|
63
|
+
) {
|
|
64
|
+
throw new Error('expected textured material slots');
|
|
65
|
+
}
|
|
66
|
+
expect(material.baseColorTexture.texCoord).toBe(6);
|
|
67
|
+
expect(material.baseColorTexture.transform).toEqual({
|
|
68
|
+
offset: [0.25, 0.5],
|
|
69
|
+
rotation: 0.75,
|
|
70
|
+
scale: [2, 3],
|
|
71
|
+
});
|
|
72
|
+
expect(material.metallicRoughnessTexture.texCoord).toBe(7);
|
|
73
|
+
expect(material.metallicRoughnessTexture.transform).toEqual({
|
|
74
|
+
offset: [-0.25, -0.5],
|
|
75
|
+
rotation: -0.75,
|
|
76
|
+
scale: [4, 5],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const materialIr = result.value.materials[0];
|
|
80
|
+
expect(materialIr).toBeDefined();
|
|
81
|
+
if (materialIr === undefined) return;
|
|
82
|
+
const projected = toMaterialAsset(materialIr, {
|
|
83
|
+
textureHandles: new Map([
|
|
84
|
+
[0, 100],
|
|
85
|
+
[1, 101],
|
|
86
|
+
]) as unknown as ReadonlyMap<number, Handle<'TextureAsset', 'shared'>>,
|
|
87
|
+
});
|
|
88
|
+
expect(projected.values).toMatchObject({
|
|
89
|
+
baseColorTexture: {
|
|
90
|
+
texture: 100,
|
|
91
|
+
coordinates: {
|
|
92
|
+
set: 6,
|
|
93
|
+
transform: { offset: [0.25, 0.5], rotation: 0.75, scale: [2, 3] },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
metallicRoughnessTexture: {
|
|
97
|
+
texture: 101,
|
|
98
|
+
coordinates: {
|
|
99
|
+
set: 7,
|
|
100
|
+
transform: { offset: [-0.25, -0.5], rotation: -0.75, scale: [4, 5] },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseGltf } from '../parse-gltf.js';
|
|
3
|
+
|
|
4
|
+
const noopLoader = async (_uri: string) => new ArrayBuffer(0);
|
|
5
|
+
|
|
6
|
+
const positionBytes = new Float32Array([0, 0, 0]);
|
|
7
|
+
const uvBytes = new Float32Array([0, 0]);
|
|
8
|
+
const positionBase64 = Buffer.from(positionBytes.buffer).toString('base64');
|
|
9
|
+
const uvBase64 = Buffer.from(uvBytes.buffer).toString('base64');
|
|
10
|
+
|
|
11
|
+
describe('glTF unused UV sets', () => {
|
|
12
|
+
it('preserves mesh UV sets that no material slot consumes', async () => {
|
|
13
|
+
const result = await parseGltf(
|
|
14
|
+
{
|
|
15
|
+
asset: { version: '2.0' },
|
|
16
|
+
meshes: [{ primitives: [{ attributes: { POSITION: 0, TEXCOORD_0: 1, TEXCOORD_3: 2 } }] }],
|
|
17
|
+
buffers: [
|
|
18
|
+
{ uri: `data:application/octet-stream;base64,${positionBase64}`, byteLength: 12 },
|
|
19
|
+
{ uri: `data:application/octet-stream;base64,${uvBase64}`, byteLength: 8 },
|
|
20
|
+
{ uri: `data:application/octet-stream;base64,${uvBase64}`, byteLength: 8 },
|
|
21
|
+
],
|
|
22
|
+
bufferViews: [
|
|
23
|
+
{ buffer: 0, byteOffset: 0, byteLength: 12 },
|
|
24
|
+
{ buffer: 1, byteOffset: 0, byteLength: 8 },
|
|
25
|
+
{ buffer: 2, byteOffset: 0, byteLength: 8 },
|
|
26
|
+
],
|
|
27
|
+
accessors: [
|
|
28
|
+
{ bufferView: 0, componentType: 5126, count: 1, type: 'VEC3' },
|
|
29
|
+
{ bufferView: 1, componentType: 5126, count: 1, type: 'VEC2' },
|
|
30
|
+
{ bufferView: 2, componentType: 5126, count: 1, type: 'VEC2' },
|
|
31
|
+
],
|
|
32
|
+
materials: [{ pbrMetallicRoughness: { baseColorFactor: [1, 1, 1, 1] } }],
|
|
33
|
+
},
|
|
34
|
+
noopLoader,
|
|
35
|
+
'/unused-uv.gltf',
|
|
36
|
+
);
|
|
37
|
+
expect(result.ok).toBe(true);
|
|
38
|
+
if (!result.ok) return;
|
|
39
|
+
expect(result.value.meshes[0]?.texcoord3).toBeInstanceOf(Float32Array);
|
|
40
|
+
expect(
|
|
41
|
+
(result.value.materials[0] as unknown as Record<string, unknown>).coordinatesSet,
|
|
42
|
+
).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { projectMeshoptBufferViews } from '../meshopt-decode.js';
|
|
3
|
+
|
|
4
|
+
function compressedView(overrides: Record<string, unknown> = {}) {
|
|
5
|
+
return {
|
|
6
|
+
buffer: 0,
|
|
7
|
+
byteOffset: 0,
|
|
8
|
+
byteLength: 8,
|
|
9
|
+
extensions: {
|
|
10
|
+
EXT_meshopt_compression: {
|
|
11
|
+
buffer: 1,
|
|
12
|
+
byteOffset: 2,
|
|
13
|
+
byteLength: 4,
|
|
14
|
+
byteStride: 4,
|
|
15
|
+
count: 2,
|
|
16
|
+
mode: 'ATTRIBUTES' as const,
|
|
17
|
+
...overrides,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('EXT_meshopt_compression projection', () => {
|
|
24
|
+
it('decodes the declared compressed range and replaces the view exactly once', async () => {
|
|
25
|
+
const source = new Uint8Array([9, 9, 1, 2, 3, 4, 9]);
|
|
26
|
+
const calls: Array<{ source: Uint8Array; count: number; stride: number }> = [];
|
|
27
|
+
const result = await projectMeshoptBufferViews(
|
|
28
|
+
[compressedView()],
|
|
29
|
+
[new Uint8Array(8), source],
|
|
30
|
+
['EXT_meshopt_compression'],
|
|
31
|
+
{
|
|
32
|
+
decode(input) {
|
|
33
|
+
calls.push(input);
|
|
34
|
+
return new Uint8Array(input.count * input.stride);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(result.ok).toBe(true);
|
|
40
|
+
expect(calls).toHaveLength(1);
|
|
41
|
+
expect(calls[0]?.source).toEqual(new Uint8Array([1, 2, 3, 4]));
|
|
42
|
+
if (result.ok) {
|
|
43
|
+
expect(result.value.decodedCount).toBe(1);
|
|
44
|
+
expect(result.value.bufferViews[0]).toMatchObject({
|
|
45
|
+
buffer: 2,
|
|
46
|
+
byteOffset: 0,
|
|
47
|
+
byteLength: 8,
|
|
48
|
+
});
|
|
49
|
+
expect(result.value.bufferViews[0]).not.toHaveProperty('extensions');
|
|
50
|
+
expect(result.value.buffers[2]).toHaveLength(8);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('preserves an optional core fallback without touching compressed bytes', async () => {
|
|
55
|
+
const result = await projectMeshoptBufferViews(
|
|
56
|
+
[compressedView()],
|
|
57
|
+
[new Uint8Array(8), new Uint8Array([1, 2, 3, 4])],
|
|
58
|
+
[],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(result.ok).toBe(true);
|
|
62
|
+
if (result.ok) {
|
|
63
|
+
expect(result.value.decodedCount).toBe(0);
|
|
64
|
+
expect(result.value.bufferViews[0]?.buffer).toBe(0);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('supports triangle and index streams through the same capability seam', async () => {
|
|
69
|
+
const modes = ['TRIANGLES', 'INDICES'] as const;
|
|
70
|
+
const seen: string[] = [];
|
|
71
|
+
for (const mode of modes) {
|
|
72
|
+
const result = await projectMeshoptBufferViews(
|
|
73
|
+
[compressedView({ mode, byteStride: 2, count: 6 })],
|
|
74
|
+
[new Uint8Array(12), new Uint8Array(8)],
|
|
75
|
+
['EXT_meshopt_compression'],
|
|
76
|
+
{
|
|
77
|
+
decode(input) {
|
|
78
|
+
seen.push(`${input.mode}:${input.filter}`);
|
|
79
|
+
return new Uint8Array(input.count * input.stride);
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
expect(result.ok).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
expect(seen).toEqual(['TRIANGLES:NONE', 'INDICES:NONE']);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { MeshoptBufferViewJson } from '../meshopt-decode.js';
|
|
3
|
+
import { projectMeshoptBufferViews } from '../meshopt-decode.js';
|
|
4
|
+
|
|
5
|
+
function view(extension: Record<string, unknown>, coreByteLength = 0) {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
buffer: 0,
|
|
9
|
+
byteLength: coreByteLength,
|
|
10
|
+
extensions: { EXT_meshopt_compression: extension },
|
|
11
|
+
},
|
|
12
|
+
] as unknown as MeshoptBufferViewJson[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const baseExtension = {
|
|
16
|
+
buffer: 1,
|
|
17
|
+
byteOffset: 0,
|
|
18
|
+
byteLength: 4,
|
|
19
|
+
byteStride: 4,
|
|
20
|
+
count: 2,
|
|
21
|
+
mode: 'ATTRIBUTES',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('EXT_meshopt_compression falsifiers', () => {
|
|
25
|
+
it('fails closed when required compressed data has no decoder', async () => {
|
|
26
|
+
const result = await projectMeshoptBufferViews(
|
|
27
|
+
view(baseExtension),
|
|
28
|
+
[new Uint8Array(0), new Uint8Array(4)],
|
|
29
|
+
['EXT_meshopt_compression'],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(result.ok).toBe(false);
|
|
33
|
+
if (!result.ok && result.error.code === 'gltf-meshopt-decoder-required') {
|
|
34
|
+
expect(result.error.code).toBe('gltf-meshopt-decoder-required');
|
|
35
|
+
expect(result.error.detail.actual).toBe('required');
|
|
36
|
+
expect(result.error.detail.hasCoreFallback).toBe(false);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects an out-of-range compressed payload before invoking the decoder', async () => {
|
|
41
|
+
let called = false;
|
|
42
|
+
const result = await projectMeshoptBufferViews(
|
|
43
|
+
view({ ...baseExtension, byteOffset: 3, byteLength: 4 }),
|
|
44
|
+
[new Uint8Array(0), new Uint8Array(4)],
|
|
45
|
+
['EXT_meshopt_compression'],
|
|
46
|
+
{
|
|
47
|
+
decode() {
|
|
48
|
+
called = true;
|
|
49
|
+
return new Uint8Array(8);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(result.ok).toBe(false);
|
|
55
|
+
expect(called).toBe(false);
|
|
56
|
+
if (!result.ok) expect(result.error.code).toBe('gltf-meshopt-decode-failed');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('rejects invalid mode/filter/count/stride declarations', async () => {
|
|
60
|
+
const invalidCases = [
|
|
61
|
+
{ mode: 'NOT_A_MODE' },
|
|
62
|
+
{ filter: 'NOT_A_FILTER' },
|
|
63
|
+
{ count: 0 },
|
|
64
|
+
{ byteStride: 0 },
|
|
65
|
+
];
|
|
66
|
+
for (const mutation of invalidCases) {
|
|
67
|
+
const result = await projectMeshoptBufferViews(
|
|
68
|
+
view({ ...baseExtension, ...mutation }),
|
|
69
|
+
[new Uint8Array(0), new Uint8Array(4)],
|
|
70
|
+
['EXT_meshopt_compression'],
|
|
71
|
+
{ decode: () => new Uint8Array(8) },
|
|
72
|
+
);
|
|
73
|
+
expect(result.ok).toBe(false);
|
|
74
|
+
if (!result.ok) expect(result.error.code).toBe('gltf-meshopt-decode-failed');
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rejects a decoder result whose byte length disagrees with count times stride', async () => {
|
|
79
|
+
const result = await projectMeshoptBufferViews(
|
|
80
|
+
view(baseExtension),
|
|
81
|
+
[new Uint8Array(0), new Uint8Array(4)],
|
|
82
|
+
['EXT_meshopt_compression'],
|
|
83
|
+
{ decode: () => new Uint8Array(7) },
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(result.ok).toBe(false);
|
|
87
|
+
if (!result.ok && result.error.code === 'gltf-meshopt-decode-failed') {
|
|
88
|
+
expect(result.error.code).toBe('gltf-meshopt-decode-failed');
|
|
89
|
+
expect(result.error.detail.actual).toContain('expected=8');
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|