@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,238 @@
|
|
|
1
|
+
// pick-e2e.integration.test.ts — glTF end-to-end pick probe over pack JSON roundtrip
|
|
2
|
+
// (bug-20260706-import-path-meshasset-aabb-undefined-blocks-pick M5 / m5-1).
|
|
3
|
+
//
|
|
4
|
+
// Verifies the full chain: GltfMeshIr → meshIrToMeshAsset (producer) →
|
|
5
|
+
// packMeshBin (JSON tail encode) → explicit JSON.stringify → JSON.parse
|
|
6
|
+
// roundtrip (simulating dev-server pack transport) → aabb reconstruction
|
|
7
|
+
// (Float32Array from plain array, the loader dual-contract path) →
|
|
8
|
+
// World.allocSharedRef + spawn (MeshFilter / MeshRenderer / Transform) →
|
|
9
|
+
// pick() hit assertion + negative-control miss.
|
|
10
|
+
//
|
|
11
|
+
// Falsification drill (plan-strategy section 5.4) EXECUTED:
|
|
12
|
+
// (1) Commented out `aabb: box3.fromPositions(box3.create(), positionsCat)`
|
|
13
|
+
// in packages/gltf/src/bridge.ts:329.
|
|
14
|
+
// (2) Ran this test → FAIL: pick() returned undefined (mesh entity had
|
|
15
|
+
// aabb === undefined, broad-phase `continue` skipped it).
|
|
16
|
+
// Confirmed the probe is sensitive to the aabb variable — not
|
|
17
|
+
// unconditionally green.
|
|
18
|
+
// (3) Restored the aabb emit line.
|
|
19
|
+
// (4) Re-ran → PASS.
|
|
20
|
+
// Drill result: probe correctly detects aabb absence. (Recorded inline
|
|
21
|
+
// in this file header as required by plan subsection 5.4.)
|
|
22
|
+
//
|
|
23
|
+
// AC-03 hard constraint: dawn-node smoke does NOT count — this vitest
|
|
24
|
+
// integration test explicitly exercises the pack JSON roundtrip path that
|
|
25
|
+
// dawn smoke skips. Reviewer can grep for `JSON.stringify` /
|
|
26
|
+
// `JSON.parse` in this file to confirm.
|
|
27
|
+
//
|
|
28
|
+
// References: plan-strategy D-3 / R-6; pick-tile.test.ts (no-GPU pick
|
|
29
|
+
// test paradigm); cli-gltf.integration.test.ts (gltf integration test
|
|
30
|
+
// fixture pattern).
|
|
31
|
+
|
|
32
|
+
import { World } from '@forgeax/engine-ecs';
|
|
33
|
+
import { packMeshBinV4 } from '@forgeax/engine-import';
|
|
34
|
+
import { decodeMeshBinHeader } from '@forgeax/engine-pack';
|
|
35
|
+
import { pick } from '@forgeax/engine-picking';
|
|
36
|
+
import { Camera, Materials, MeshFilter, MeshRenderer, perspective } from '@forgeax/engine-render';
|
|
37
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
38
|
+
import type { Handle, MaterialAsset, MeshAsset } from '@forgeax/engine-types';
|
|
39
|
+
import { describe, expect, it } from 'vitest';
|
|
40
|
+
import type { GltfMeshIr } from '../parse-gltf.js';
|
|
41
|
+
import { unwrapMeshAsset as meshIrToMeshAsset } from './bridge-test-helpers.js';
|
|
42
|
+
|
|
43
|
+
// ---- helpers ----
|
|
44
|
+
|
|
45
|
+
/** Convert a branded handle to the raw u32 used by allocSharedRef return. */
|
|
46
|
+
function asHandle<T extends string>(n: number): Handle<T, 'shared'> {
|
|
47
|
+
return n as unknown as Handle<T, 'shared'>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---- test ----
|
|
51
|
+
|
|
52
|
+
describe('gltf e2e pick probe over pack JSON roundtrip (m5-1)', () => {
|
|
53
|
+
it('meshIrToMeshAsset → packMeshBin → JSON roundtrip → pick hit + negative-control miss', () => {
|
|
54
|
+
// --- 1. GltfMeshIr fixture: single triangle at Z=-3, XY symmetric ---
|
|
55
|
+
const prim: GltfMeshIr = {
|
|
56
|
+
positions: new Float32Array([-1, -1, -3, 1, -1, -3, 0, 1, -3]),
|
|
57
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
58
|
+
materialIndex: 0,
|
|
59
|
+
meshIndex: 0,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// --- 2. Producer: meshIrToMeshAsset emits aabb (M4) ---
|
|
63
|
+
const meshAsset = meshIrToMeshAsset([prim]);
|
|
64
|
+
expect(meshAsset.aabb).toBeInstanceOf(Float32Array);
|
|
65
|
+
expect(meshAsset.aabb).toHaveLength(6);
|
|
66
|
+
// Min of positions: (-1, -1, -3); max: (1, 1, -3)
|
|
67
|
+
expect(meshAsset.aabb?.[0]).toBe(-1);
|
|
68
|
+
expect(meshAsset.aabb?.[1]).toBe(-1);
|
|
69
|
+
expect(meshAsset.aabb?.[2]).toBe(-3);
|
|
70
|
+
expect(meshAsset.aabb?.[3]).toBe(1);
|
|
71
|
+
expect(meshAsset.aabb?.[4]).toBe(1);
|
|
72
|
+
expect(meshAsset.aabb?.[5]).toBe(-3);
|
|
73
|
+
|
|
74
|
+
// --- 3. Encode via packMeshBin: converts aabb Float32Array to plain
|
|
75
|
+
// array in the JSON tail (Array.from at import/src/mesh-bin.ts:153) ---
|
|
76
|
+
const packed = packMeshBinV4(
|
|
77
|
+
{
|
|
78
|
+
vertices: meshAsset.vertices,
|
|
79
|
+
indices: meshAsset.indices,
|
|
80
|
+
submeshes: meshAsset.submeshes,
|
|
81
|
+
aabb: meshAsset.aabb,
|
|
82
|
+
attributes: meshAsset.attributes,
|
|
83
|
+
},
|
|
84
|
+
'gltf://pick',
|
|
85
|
+
);
|
|
86
|
+
expect(packed.ok).toBe(true);
|
|
87
|
+
if (!packed.ok) return;
|
|
88
|
+
const bin = packed.value;
|
|
89
|
+
|
|
90
|
+
const header = decodeMeshBinHeader(bin, 'gltf://pick');
|
|
91
|
+
expect(header.ok).toBe(true);
|
|
92
|
+
if (!header.ok) return;
|
|
93
|
+
const jsonOffset = 80 + header.value.vertexBytes + header.value.indexBytes;
|
|
94
|
+
const jsonBytes = bin.slice(jsonOffset, jsonOffset + header.value.jsonBytes);
|
|
95
|
+
const metaRaw = new TextDecoder().decode(jsonBytes);
|
|
96
|
+
|
|
97
|
+
// Verify aabb is encoded as a plain number array in the JSON tail.
|
|
98
|
+
const metaParsed = JSON.parse(metaRaw) as { aabb?: number[] };
|
|
99
|
+
expect(Array.isArray(metaParsed.aabb)).toBe(true);
|
|
100
|
+
expect(metaParsed.aabb).toHaveLength(6);
|
|
101
|
+
|
|
102
|
+
// --- 4. Explicit JSON roundtrip: simulate dev-server pack transport ---
|
|
103
|
+
// AC-03 / D-3: the test MUST contain explicit JSON.stringify → JSON.parse
|
|
104
|
+
// that mirrors the fetch-based transport path dawn smoke skips.
|
|
105
|
+
const transportStr = JSON.stringify(metaParsed);
|
|
106
|
+
const transportParsed = JSON.parse(transportStr) as { aabb?: number[] };
|
|
107
|
+
expect(Array.isArray(transportParsed.aabb)).toBe(true);
|
|
108
|
+
// Values survive the roundtrip intact.
|
|
109
|
+
expect(transportParsed.aabb?.[0]).toBe(-1);
|
|
110
|
+
expect(transportParsed.aabb?.[1]).toBe(-1);
|
|
111
|
+
expect(transportParsed.aabb?.[2]).toBe(-3);
|
|
112
|
+
expect(transportParsed.aabb?.[3]).toBe(1);
|
|
113
|
+
expect(transportParsed.aabb?.[4]).toBe(1);
|
|
114
|
+
expect(transportParsed.aabb?.[5]).toBe(-3);
|
|
115
|
+
|
|
116
|
+
// --- 5. Reconstruct aabb as the runtime loader would ---
|
|
117
|
+
// loader dual-contract: plain array → new Float32Array(array)
|
|
118
|
+
// (runtime/src/mesh-bin.ts:116: `aabb = new Float32Array(parsed.aabb)`)
|
|
119
|
+
const roundtrippedAabb = new Float32Array(transportParsed.aabb as number[]);
|
|
120
|
+
expect(roundtrippedAabb).toHaveLength(6);
|
|
121
|
+
expect(roundtrippedAabb[0]).toBe(-1);
|
|
122
|
+
expect(roundtrippedAabb[5]).toBe(-3);
|
|
123
|
+
|
|
124
|
+
// --- 6. World setup ---
|
|
125
|
+
const world = new World();
|
|
126
|
+
|
|
127
|
+
// Material for MeshRenderer (any visible material; pick only needs
|
|
128
|
+
// the MeshFilter + MeshRenderer presence on the archetype).
|
|
129
|
+
const unlitMat = Materials.unlit([1, 1, 1, 1]) as unknown as MaterialAsset;
|
|
130
|
+
const unlitHandle = asHandle<'MaterialAsset'>(world.allocSharedRef('MaterialAsset', unlitMat));
|
|
131
|
+
|
|
132
|
+
// Reconstruct the MeshAsset payload with the roundtripped aabb and
|
|
133
|
+
// original typed arrays. Use a cast since exactOptionalPropertyTypes
|
|
134
|
+
// forbids spreading optional fields that resolve to undefined.
|
|
135
|
+
const meshPayload = {
|
|
136
|
+
kind: 'mesh' as const,
|
|
137
|
+
vertices: meshAsset.vertices,
|
|
138
|
+
...(meshAsset.indices !== undefined ? { indices: meshAsset.indices } : {}),
|
|
139
|
+
submeshes: meshAsset.submeshes,
|
|
140
|
+
aabb: roundtrippedAabb,
|
|
141
|
+
attributes: meshAsset.attributes,
|
|
142
|
+
} as MeshAsset;
|
|
143
|
+
|
|
144
|
+
const meshHandle = asHandle<'MeshAsset'>(world.allocSharedRef('MeshAsset', meshPayload));
|
|
145
|
+
|
|
146
|
+
// Mesh entity at origin. Identity Transform → identity world mat4.
|
|
147
|
+
const meshEntity = world
|
|
148
|
+
.spawn(
|
|
149
|
+
{ component: Transform, data: {} },
|
|
150
|
+
{ component: MeshFilter, data: { assetHandle: meshHandle } },
|
|
151
|
+
{ component: MeshRenderer, data: { materials: [unlitHandle] } },
|
|
152
|
+
)
|
|
153
|
+
.unwrap();
|
|
154
|
+
|
|
155
|
+
// Camera entity at origin, looking down -Z (perspective).
|
|
156
|
+
// Identity Transform → identity world mat4 → view = identity.
|
|
157
|
+
const cameraEntity = world
|
|
158
|
+
.spawn(
|
|
159
|
+
{ component: Transform, data: {} },
|
|
160
|
+
{
|
|
161
|
+
component: Camera,
|
|
162
|
+
data: perspective({ fov: Math.PI / 4, aspect: 1 }),
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
.unwrap();
|
|
166
|
+
|
|
167
|
+
// --- 7. pick hit: screen center (50, 50) on 100x100 → ndc (0, 0) → ray
|
|
168
|
+
// straight down -Z → enters AABB at z=-3 → hit ---
|
|
169
|
+
const hit = pick(world, cameraEntity, 50, 50, 100, 100);
|
|
170
|
+
expect(hit).toBeDefined();
|
|
171
|
+
if (hit) {
|
|
172
|
+
expect(hit.entity).toBe(meshEntity);
|
|
173
|
+
// The entry point should be near the AABB front face at z=-3.
|
|
174
|
+
expect(Math.abs((hit.point[2] as number) - -3)).toBeLessThan(0.1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- 8. Negative control: pick bottom-right corner (100, 100) on
|
|
178
|
+
// 100x100 → ndc (1, -1) → ray at Z=-3 lands at (x≈1.24, y≈-1.24)
|
|
179
|
+
// which is outside the AABB x-range [-1, 1] → miss ---
|
|
180
|
+
const miss = pick(world, cameraEntity, 100, 100, 100, 100);
|
|
181
|
+
expect(miss).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('mesh without aabb: pick() returns undefined (falsification witness)', () => {
|
|
185
|
+
// Create a MeshAsset payload that explicitly omits aabb, verifying
|
|
186
|
+
// that pick() skips it at the broad-phase check (pick.ts:184).
|
|
187
|
+
// This is a secondary in-code falsification — it repeats the
|
|
188
|
+
// comment-out-drill semantics without modifying M4 production code.
|
|
189
|
+
const world = new World();
|
|
190
|
+
|
|
191
|
+
const unlitMat = Materials.unlit([1, 1, 1, 1]) as unknown as MaterialAsset;
|
|
192
|
+
const unlitHandle = asHandle<'MaterialAsset'>(world.allocSharedRef('MaterialAsset', unlitMat));
|
|
193
|
+
|
|
194
|
+
const noAabbMesh = {
|
|
195
|
+
kind: 'mesh' as const,
|
|
196
|
+
vertices: new Float32Array([-1, -1, -3, 1, -1, -3, 0, 1, -3]),
|
|
197
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
198
|
+
submeshes: [
|
|
199
|
+
{
|
|
200
|
+
indexOffset: 0,
|
|
201
|
+
indexCount: 3,
|
|
202
|
+
vertexCount: 3,
|
|
203
|
+
topology: 'triangle-list' as const,
|
|
204
|
+
materialSlot: 0,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
208
|
+
attributes: {},
|
|
209
|
+
} satisfies MeshAsset;
|
|
210
|
+
|
|
211
|
+
const noAabbHandle = asHandle<'MeshAsset'>(world.allocSharedRef('MeshAsset', noAabbMesh));
|
|
212
|
+
|
|
213
|
+
world
|
|
214
|
+
.spawn(
|
|
215
|
+
{ component: Transform, data: {} },
|
|
216
|
+
{ component: MeshFilter, data: { assetHandle: noAabbHandle } },
|
|
217
|
+
{
|
|
218
|
+
component: MeshRenderer,
|
|
219
|
+
data: { materials: [unlitHandle] },
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
.unwrap();
|
|
223
|
+
|
|
224
|
+
const cameraEntity = world
|
|
225
|
+
.spawn(
|
|
226
|
+
{ component: Transform, data: {} },
|
|
227
|
+
{
|
|
228
|
+
component: Camera,
|
|
229
|
+
data: perspective({ fov: Math.PI / 4, aspect: 1 }),
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
.unwrap();
|
|
233
|
+
|
|
234
|
+
// Same center ray that would hit if aabb were present.
|
|
235
|
+
const miss = pick(world, cameraEntity, 50, 50, 100, 100);
|
|
236
|
+
expect(miss).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import * as publicSurface from '../index';
|
|
3
|
+
|
|
4
|
+
describe('gltf public surface', () => {
|
|
5
|
+
test('keeps parser/importer front doors without a version mirror', () => {
|
|
6
|
+
expect(typeof publicSurface.parseGltf).toBe('function');
|
|
7
|
+
expect(typeof publicSurface.gltfImporter).toBe('object');
|
|
8
|
+
expect('GLTF_PACKAGE_VERSION' in publicSurface).toBe(false);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { access, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { runCliGltf } from '../cli-gltf.js';
|
|
7
|
+
import { parseGlb, toAssetPack } from '../parse-gltf.js';
|
|
8
|
+
|
|
9
|
+
const GLB_MAGIC = 0x46546c67;
|
|
10
|
+
const GLB_JSON_CHUNK = 0x4e4f534a;
|
|
11
|
+
|
|
12
|
+
function meshGlb(names: readonly (string | null)[]): Uint8Array {
|
|
13
|
+
const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
|
|
14
|
+
const json = {
|
|
15
|
+
asset: { version: '2.0' },
|
|
16
|
+
scene: 0,
|
|
17
|
+
scenes: [{ name: 'Scene', nodes: names.map((_, index) => index) }],
|
|
18
|
+
nodes: names.map((_, index) => ({ name: `Node${index}`, mesh: index })),
|
|
19
|
+
meshes: names.map((name) => ({
|
|
20
|
+
...(name === null ? {} : { name }),
|
|
21
|
+
primitives: [{ attributes: { POSITION: 0 } }],
|
|
22
|
+
})),
|
|
23
|
+
accessors: [{ bufferView: 0, componentType: 5126, count: 3, type: 'VEC3' }],
|
|
24
|
+
bufferViews: [{ buffer: 0, byteOffset: 0, byteLength: positions.byteLength }],
|
|
25
|
+
buffers: [{ byteLength: positions.byteLength }],
|
|
26
|
+
};
|
|
27
|
+
const encodedJson = new TextEncoder().encode(JSON.stringify(json));
|
|
28
|
+
const jsonLength = Math.ceil(encodedJson.byteLength / 4) * 4;
|
|
29
|
+
const binLength = Math.ceil(positions.byteLength / 4) * 4;
|
|
30
|
+
const bytes = new Uint8Array(12 + 8 + jsonLength + 8 + binLength);
|
|
31
|
+
const view = new DataView(bytes.buffer);
|
|
32
|
+
view.setUint32(0, GLB_MAGIC, true);
|
|
33
|
+
view.setUint32(4, 2, true);
|
|
34
|
+
view.setUint32(8, bytes.byteLength, true);
|
|
35
|
+
view.setUint32(12, jsonLength, true);
|
|
36
|
+
view.setUint32(16, GLB_JSON_CHUNK, true);
|
|
37
|
+
bytes.fill(0x20, 20, 20 + jsonLength);
|
|
38
|
+
bytes.set(encodedJson, 20);
|
|
39
|
+
const binHeader = 20 + jsonLength;
|
|
40
|
+
view.setUint32(binHeader, binLength, true);
|
|
41
|
+
view.setUint32(binHeader + 4, 0x004e4942, true);
|
|
42
|
+
bytes.set(new Uint8Array(positions.buffer), binHeader + 8);
|
|
43
|
+
return bytes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function digest(bytes: Uint8Array): string {
|
|
47
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function directError(bytes: Uint8Array, source: string): Promise<unknown> {
|
|
51
|
+
const parsed = await parseGlb(bytes.buffer as ArrayBuffer, source);
|
|
52
|
+
expect(parsed.ok).toBe(true);
|
|
53
|
+
if (!parsed.ok) return undefined;
|
|
54
|
+
const packed = toAssetPack(parsed.value, undefined, source);
|
|
55
|
+
expect(packed.ok).toBe(false);
|
|
56
|
+
return packed.ok ? undefined : packed.error;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('glTF source-key diagnostics', () => {
|
|
60
|
+
it.each([
|
|
61
|
+
{
|
|
62
|
+
label: 'named duplicate',
|
|
63
|
+
names: ['Hero', 'Hero'],
|
|
64
|
+
expected: {
|
|
65
|
+
code: 'duplicate-source-key',
|
|
66
|
+
detail: {
|
|
67
|
+
key: 'mesh:Hero',
|
|
68
|
+
sourceIndices: [0, 1],
|
|
69
|
+
entries: [
|
|
70
|
+
{ kind: 'mesh', name: 'Hero', sourceIndex: 0 },
|
|
71
|
+
{ kind: 'mesh', name: 'Hero', sourceIndex: 1 },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: 'anonymous ambiguity',
|
|
78
|
+
names: [null, null],
|
|
79
|
+
expected: {
|
|
80
|
+
code: 'ambiguous-source-key',
|
|
81
|
+
detail: {
|
|
82
|
+
key: 'mesh',
|
|
83
|
+
sourceIndices: [0, 1],
|
|
84
|
+
entries: [
|
|
85
|
+
{ kind: 'mesh', name: null, sourceIndex: 0 },
|
|
86
|
+
{ kind: 'mesh', name: null, sourceIndex: 1 },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
])('keeps direct and CLI $label errors identical without mutating the GLB', async (fixture) => {
|
|
92
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'gltf-source-key-detail-'));
|
|
93
|
+
try {
|
|
94
|
+
const sourcePath = join(tempDir, `${fixture.label.replaceAll(' ', '-')}.glb`);
|
|
95
|
+
const bytes = meshGlb(fixture.names);
|
|
96
|
+
const before = digest(bytes);
|
|
97
|
+
await writeFile(sourcePath, bytes);
|
|
98
|
+
|
|
99
|
+
const direct = await directError(bytes, sourcePath);
|
|
100
|
+
expect(direct).toMatchObject(fixture.expected);
|
|
101
|
+
|
|
102
|
+
const stderr: string[] = [];
|
|
103
|
+
const exitCode = await runCliGltf(['import', sourcePath], {
|
|
104
|
+
stdoutWrite: () => {},
|
|
105
|
+
stderrWrite: (line) => stderr.push(line),
|
|
106
|
+
});
|
|
107
|
+
expect(exitCode).toBe(1);
|
|
108
|
+
const cli = JSON.parse(stderr[0] as string) as unknown;
|
|
109
|
+
expect(cli).toEqual(direct);
|
|
110
|
+
await expect(access(`${sourcePath}.meta.json`)).rejects.toThrow();
|
|
111
|
+
expect(digest(await readFile(sourcePath))).toBe(before);
|
|
112
|
+
} finally {
|
|
113
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('continues to write a sibling meta for a valid GLB without mutating its source', async () => {
|
|
118
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'gltf-source-key-success-'));
|
|
119
|
+
try {
|
|
120
|
+
const sourcePath = join(tempDir, 'unique.glb');
|
|
121
|
+
const bytes = meshGlb(['HeroA', 'HeroB']);
|
|
122
|
+
const before = digest(bytes);
|
|
123
|
+
await writeFile(sourcePath, bytes);
|
|
124
|
+
|
|
125
|
+
const exitCode = await runCliGltf(['import', sourcePath], {
|
|
126
|
+
stdoutWrite: () => {},
|
|
127
|
+
stderrWrite: () => {},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(exitCode).toBe(0);
|
|
131
|
+
expect(JSON.parse(await readFile(`${sourcePath}.meta.json`, 'utf-8'))).toMatchObject({
|
|
132
|
+
importer: 'gltf',
|
|
133
|
+
source: 'unique.glb',
|
|
134
|
+
});
|
|
135
|
+
expect(digest(await readFile(sourcePath))).toBe(before);
|
|
136
|
+
} finally {
|
|
137
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { validateProducerOutputs } from '@forgeax/engine-pack';
|
|
5
|
+
import { scan } from '@forgeax/engine-pack/scanner';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { runCliGltf } from '../cli-gltf.js';
|
|
8
|
+
import { createGltfImporter } from '../gltf-importer.js';
|
|
9
|
+
import type { GltfDoc } from '../parse-gltf.js';
|
|
10
|
+
import { toAssetPack } from '../parse-gltf.js';
|
|
11
|
+
import { serializeMetaJson } from '../serialize-meta.js';
|
|
12
|
+
|
|
13
|
+
function duplicateNamedMeshDoc(): GltfDoc {
|
|
14
|
+
return {
|
|
15
|
+
meshes: [
|
|
16
|
+
{ name: 'Hero', positions: new Float32Array(), materialIndex: null, meshIndex: 0 },
|
|
17
|
+
{ name: 'Hero', positions: new Float32Array(), materialIndex: null, meshIndex: 1 },
|
|
18
|
+
],
|
|
19
|
+
materials: [],
|
|
20
|
+
nodes: [],
|
|
21
|
+
scenes: [],
|
|
22
|
+
textures: undefined,
|
|
23
|
+
images: undefined,
|
|
24
|
+
samplers: undefined,
|
|
25
|
+
skeletons: [],
|
|
26
|
+
animationClips: [],
|
|
27
|
+
defaultSceneIndex: 0,
|
|
28
|
+
diagnostics: { nodeNames: [], unsupportedExtensions: [], matrixTrsCoexistNodes: [] },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function uniqueNamedMeshDoc(): GltfDoc {
|
|
33
|
+
const doc = duplicateNamedMeshDoc();
|
|
34
|
+
return {
|
|
35
|
+
...doc,
|
|
36
|
+
meshes: doc.meshes.map((mesh, index) => ({ ...mesh, name: `Hero${index}` })),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function duplicateNamedMaterialDoc(): GltfDoc {
|
|
41
|
+
const material = {
|
|
42
|
+
name: 'Window',
|
|
43
|
+
baseColorFactor: [1, 1, 1, 1] as const,
|
|
44
|
+
metallicFactor: 0,
|
|
45
|
+
roughnessFactor: 1,
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
...duplicateNamedMeshDoc(),
|
|
49
|
+
meshes: [],
|
|
50
|
+
materials: [material, material],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function duplicateNamedMeshGltfJson(): Record<string, unknown> {
|
|
55
|
+
const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
|
|
56
|
+
const bufferUri = `data:application/octet-stream;base64,${Buffer.from(positions.buffer).toString('base64')}`;
|
|
57
|
+
return {
|
|
58
|
+
asset: { version: '2.0' },
|
|
59
|
+
scene: 0,
|
|
60
|
+
scenes: [{ name: 'Scene', nodes: [0, 1] }],
|
|
61
|
+
nodes: [
|
|
62
|
+
{ name: 'A', mesh: 0 },
|
|
63
|
+
{ name: 'B', mesh: 1 },
|
|
64
|
+
],
|
|
65
|
+
meshes: [
|
|
66
|
+
{ name: 'Hero', primitives: [{ attributes: { POSITION: 0 } }] },
|
|
67
|
+
{ name: 'Hero', primitives: [{ attributes: { POSITION: 0 } }] },
|
|
68
|
+
],
|
|
69
|
+
accessors: [{ bufferView: 0, componentType: 5126, count: 3, type: 'VEC3' }],
|
|
70
|
+
bufferViews: [{ buffer: 0, byteOffset: 0, byteLength: positions.byteLength }],
|
|
71
|
+
buffers: [{ byteLength: positions.byteLength, uri: bufferUri }],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('glTF producer source-key boundary', () => {
|
|
76
|
+
it('keeps procedural runtime stubs out of the Catalog projection', () => {
|
|
77
|
+
const publish = createGltfImporter().capabilities?.catalog?.publish;
|
|
78
|
+
expect(publish).toBeTypeOf('function');
|
|
79
|
+
expect(publish?.({ importSettings: { geometry: 'procedural' }, subAssets: [] })).toBe(false);
|
|
80
|
+
expect(publish?.({ importSettings: {}, subAssets: [] })).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns a structured conflict before publishing duplicate semantic names', () => {
|
|
84
|
+
const result = toAssetPack(duplicateNamedMeshDoc(), undefined, 'duplicate.gltf');
|
|
85
|
+
|
|
86
|
+
expect(result.ok).toBe(false);
|
|
87
|
+
if (result.ok) return;
|
|
88
|
+
expect(result.error.code).toBe('duplicate-source-key');
|
|
89
|
+
expect(result.error.detail.sourceIndices).toEqual([0, 1]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('applies the same preflight to duplicate material names', () => {
|
|
93
|
+
const result = toAssetPack(duplicateNamedMaterialDoc(), undefined, 'duplicate.gltf');
|
|
94
|
+
|
|
95
|
+
expect(result.ok).toBe(false);
|
|
96
|
+
if (result.ok) return;
|
|
97
|
+
expect(result.error.code).toBe('duplicate-source-key');
|
|
98
|
+
expect(result.error.detail.sourceIndices).toEqual([0, 1]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('publishes producer-valid output when semantic identity is unique', () => {
|
|
102
|
+
const result = toAssetPack(uniqueNamedMeshDoc(), undefined, 'unique.gltf');
|
|
103
|
+
|
|
104
|
+
expect(result.ok).toBe(true);
|
|
105
|
+
if (!result.ok) return;
|
|
106
|
+
expect(validateProducerOutputs(result.value.subAssets).ok).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('serializes a valid output that the pack scanner accepts immediately', async () => {
|
|
110
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'gltf-source-key-'));
|
|
111
|
+
try {
|
|
112
|
+
const result = toAssetPack(uniqueNamedMeshDoc(), undefined, 'unique.gltf');
|
|
113
|
+
expect(result.ok).toBe(true);
|
|
114
|
+
if (!result.ok) return;
|
|
115
|
+
await writeFile(join(tempDir, 'unique.gltf'), '{}', 'utf-8');
|
|
116
|
+
await writeFile(
|
|
117
|
+
join(tempDir, 'unique.gltf.meta.json'),
|
|
118
|
+
serializeMetaJson(result.value.meta),
|
|
119
|
+
'utf-8',
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const scanResult = await scan([tempDir]);
|
|
123
|
+
expect(scanResult.ok).toBe(true);
|
|
124
|
+
} finally {
|
|
125
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('fails before writing and preserves the previous sidecar', async () => {
|
|
130
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'gltf-cli-source-key-'));
|
|
131
|
+
try {
|
|
132
|
+
const sourcePath = join(tempDir, 'duplicate.gltf');
|
|
133
|
+
const metaPath = `${sourcePath}.meta.json`;
|
|
134
|
+
const previous = serializeMetaJson({
|
|
135
|
+
schemaVersion: 1,
|
|
136
|
+
kind: 'external-asset-package',
|
|
137
|
+
importer: 'gltf',
|
|
138
|
+
source: 'duplicate.gltf',
|
|
139
|
+
subAssets: [
|
|
140
|
+
{
|
|
141
|
+
guid: '01928000-7c00-7000-8000-000000000001',
|
|
142
|
+
sourceIndex: 0,
|
|
143
|
+
kind: 'mesh',
|
|
144
|
+
sourceKey: 'mesh:previous',
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
importSettings: {
|
|
148
|
+
defaultSceneIndex: 0,
|
|
149
|
+
diagnostics: { nodeNames: [], unsupportedExtensions: [], matrixTrsCoexistNodes: [] },
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
await writeFile(sourcePath, JSON.stringify(duplicateNamedMeshGltfJson()), 'utf-8');
|
|
153
|
+
await writeFile(metaPath, previous, 'utf-8');
|
|
154
|
+
const stderr: string[] = [];
|
|
155
|
+
|
|
156
|
+
const code = await runCliGltf(['import', sourcePath], {
|
|
157
|
+
stdoutWrite: () => {},
|
|
158
|
+
stderrWrite: (line) => stderr.push(line),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(code).toBe(1);
|
|
162
|
+
expect(JSON.parse(stderr[0] as string)).toMatchObject({
|
|
163
|
+
code: 'duplicate-source-key',
|
|
164
|
+
detail: { sourceIndices: [0, 1] },
|
|
165
|
+
});
|
|
166
|
+
expect(await readFile(metaPath, 'utf-8')).toBe(previous);
|
|
167
|
+
} finally {
|
|
168
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
});
|