@forgeax/engine-import 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +226 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/decode-image-owner.test-d.d.ts +2 -0
- package/dist/__tests__/decode-image-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/import-contract-migration.test.d.ts +2 -0
- package/dist/__tests__/import-contract-migration.test.d.ts.map +1 -0
- package/dist/__tests__/import-dependencies.test.d.ts +2 -0
- package/dist/__tests__/import-dependencies.test.d.ts.map +1 -0
- package/dist/__tests__/import-finalizer-digest-retry.test.d.ts +2 -0
- package/dist/__tests__/import-finalizer-digest-retry.test.d.ts.map +1 -0
- package/dist/__tests__/import-local-artifacts.test.d.ts +2 -0
- package/dist/__tests__/import-local-artifacts.test.d.ts.map +1 -0
- package/dist/__tests__/import.unit.test.d.ts +2 -0
- package/dist/__tests__/import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/material-import-product.unit.test.d.ts +2 -0
- package/dist/__tests__/material-import-product.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-contract.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-contract.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-output-producers.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-output-producers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-production-producers.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-production-producers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack-staged-snapshot.unit.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack-staged-snapshot.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scriptable-pack.integration.test.d.ts +2 -0
- package/dist/__tests__/scriptable-pack.integration.test.d.ts.map +1 -0
- package/dist/__tests__/source-overrides-import-context.unit.test.d.ts +2 -0
- package/dist/__tests__/source-overrides-import-context.unit.test.d.ts.map +1 -0
- package/dist/import-product.d.ts +36 -0
- package/dist/import-product.d.ts.map +1 -0
- package/dist/import-runner.d.ts +133 -0
- package/dist/import-runner.d.ts.map +1 -0
- package/dist/importer-registry.d.ts +40 -0
- package/dist/importer-registry.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +1607 -0
- package/dist/index.mjs.map +1 -0
- package/dist/mesh-bin.d.ts +19 -0
- package/dist/mesh-bin.d.ts.map +1 -0
- package/dist/scriptable-pack-output-producers.d.ts +6 -0
- package/dist/scriptable-pack-output-producers.d.ts.map +1 -0
- package/dist/scriptable-pack-staged-snapshot.d.ts +22 -0
- package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -0
- package/dist/scriptable-pack.d.ts +153 -0
- package/dist/scriptable-pack.d.ts.map +1 -0
- package/package.json +63 -0
- package/src/__tests__/decode-image-owner.test-d.ts +53 -0
- package/src/__tests__/import-contract-migration.test.ts +190 -0
- package/src/__tests__/import-dependencies.test.ts +65 -0
- package/src/__tests__/import-finalizer-digest-retry.test.ts +113 -0
- package/src/__tests__/import-local-artifacts.test.ts +100 -0
- package/src/__tests__/import.unit.test.ts +429 -0
- package/src/__tests__/material-import-product.unit.test.ts +56 -0
- package/src/__tests__/mesh-bin-contract.test.ts +95 -0
- package/src/__tests__/mesh-bin-morph-v3.test.ts +44 -0
- package/src/__tests__/mesh-bin.test.ts +102 -0
- package/src/__tests__/scriptable-pack-output-producers.unit.test.ts +190 -0
- package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +429 -0
- package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +258 -0
- package/src/__tests__/scriptable-pack.integration.test.ts +435 -0
- package/src/__tests__/source-overrides-import-context.unit.test.ts +92 -0
- package/src/import-product.ts +195 -0
- package/src/import-runner.ts +720 -0
- package/src/importer-registry.ts +80 -0
- package/src/index.ts +103 -0
- package/src/mesh-bin.ts +229 -0
- package/src/scriptable-pack-output-producers.ts +466 -0
- package/src/scriptable-pack-staged-snapshot.ts +161 -0
- package/src/scriptable-pack.ts +615 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { packMeshBinV4 } from '@forgeax/engine-import';
|
|
2
|
+
import { decodeMeshBinHeader } from '@forgeax/engine-pack';
|
|
3
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
function buildPayload(vertexCount: number, extraUvCount: number, hasSkin: boolean) {
|
|
7
|
+
const baseFpv = hasSkin ? 18 : 12;
|
|
8
|
+
const fpv = baseFpv + extraUvCount * 2;
|
|
9
|
+
const vertices = new Float32Array(vertexCount * fpv);
|
|
10
|
+
for (let i = 0; i < vertices.length; i++) vertices[i] = i * 0.1;
|
|
11
|
+
const attributes: Record<string, unknown> = {
|
|
12
|
+
position: new Float32Array(vertexCount * 3),
|
|
13
|
+
normal: new Float32Array(vertexCount * 3),
|
|
14
|
+
uv: new Float32Array(vertexCount * 2),
|
|
15
|
+
tangent: new Float32Array(vertexCount * 4),
|
|
16
|
+
};
|
|
17
|
+
for (let set = 1; set <= extraUvCount; set++) {
|
|
18
|
+
attributes[`uv${set}`] = new Float32Array(vertexCount * 2);
|
|
19
|
+
}
|
|
20
|
+
if (hasSkin) {
|
|
21
|
+
attributes.skinIndex = new Uint16Array(vertexCount * 4);
|
|
22
|
+
attributes.skinWeight = new Float32Array(vertexCount * 4);
|
|
23
|
+
}
|
|
24
|
+
return { vertices, indices: new Uint16Array([0, 1, 2]), attributes };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('mesh-bin v4 roundtrip contract', () => {
|
|
28
|
+
it.each([
|
|
29
|
+
[0, false, 1],
|
|
30
|
+
[1, false, 2],
|
|
31
|
+
[2, false, 3],
|
|
32
|
+
[7, false, 8],
|
|
33
|
+
[1, true, 2],
|
|
34
|
+
])('records canonical projection for %i extra UV sets and skin=%s', (extra, skin, uvSets) => {
|
|
35
|
+
const result = packMeshBinV4(buildPayload(4, extra, skin), `gltf://mesh/${extra}/${skin}`);
|
|
36
|
+
expect(result.ok).toBe(true);
|
|
37
|
+
if (!result.ok) return;
|
|
38
|
+
const header = decodeMeshBinHeader(result.value, `gltf://mesh/${extra}/${skin}`);
|
|
39
|
+
expect(header.ok).toBe(true);
|
|
40
|
+
if (!header.ok) return;
|
|
41
|
+
expect(header.value.version).toBe(4);
|
|
42
|
+
expect(header.value.projectionVersion).toBe(1);
|
|
43
|
+
expect(header.value.vertexCount).toBe(4);
|
|
44
|
+
expect(header.value.stride).toBe((skin ? 18 + extra * 2 : 12 + extra * 2) * 4);
|
|
45
|
+
expect(header.value.mask).toBeGreaterThan(0);
|
|
46
|
+
expect(uvSets).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('stores canonical interleaved bytes and material refs as indexes', () => {
|
|
50
|
+
const guid = '019d0000-0000-7000-8000-000000000005';
|
|
51
|
+
const parsed = AssetGuid.parse(guid);
|
|
52
|
+
if (!parsed.ok) throw new Error('fixture guid must parse');
|
|
53
|
+
const payload = {
|
|
54
|
+
vertices: new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
|
|
55
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
56
|
+
attributes: {
|
|
57
|
+
position: new Float32Array([1, 2, 3]),
|
|
58
|
+
normal: new Float32Array([4, 5, 6]),
|
|
59
|
+
uv: new Float32Array([7, 8]),
|
|
60
|
+
tangent: new Float32Array([9, 10, 11, 12]),
|
|
61
|
+
},
|
|
62
|
+
submeshes: [
|
|
63
|
+
{
|
|
64
|
+
indexOffset: 0,
|
|
65
|
+
indexCount: 3,
|
|
66
|
+
vertexCount: 1,
|
|
67
|
+
topology: 'triangle-list' as const,
|
|
68
|
+
materialSlot: 0,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
materialSlots: [
|
|
72
|
+
{ slotName: 'Body', sourceKey: 'gltf:material:0', defaultMaterial: parsed.value },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
const result = packMeshBinV4(payload, 'gltf://color', [guid]);
|
|
76
|
+
expect(result.ok).toBe(true);
|
|
77
|
+
if (!result.ok) return;
|
|
78
|
+
const header = decodeMeshBinHeader(result.value, 'gltf://color');
|
|
79
|
+
expect(header.ok).toBe(true);
|
|
80
|
+
if (!header.ok) return;
|
|
81
|
+
const jsonOffset = 80 + header.value.vertexBytes + header.value.indexBytes;
|
|
82
|
+
const json = new TextDecoder().decode(
|
|
83
|
+
result.value.subarray(jsonOffset, jsonOffset + header.value.jsonBytes),
|
|
84
|
+
);
|
|
85
|
+
expect(JSON.parse(json).materialSlots).toEqual([
|
|
86
|
+
{ slotName: 'Body', sourceKey: 'gltf:material:0', defaultMaterialRef: 0 },
|
|
87
|
+
]);
|
|
88
|
+
expect(json).not.toContain(guid);
|
|
89
|
+
expect(Array.from(result.value.subarray(80, 80 + payload.vertices.byteLength))).toEqual(
|
|
90
|
+
Array.from(new Uint8Array(payload.vertices.buffer)),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns a closed error without bytes for invalid stride cardinality', () => {
|
|
95
|
+
const payload = buildPayload(2, 0, false);
|
|
96
|
+
const result = packMeshBinV4({ ...payload, vertexCount: 3 }, 'gltf://invalid');
|
|
97
|
+
expect(result.ok).toBe(false);
|
|
98
|
+
if (result.ok) return;
|
|
99
|
+
expect(result.error.sourceKey).toBe('gltf://invalid');
|
|
100
|
+
expect(result.error.recovery).toContain('re-cook');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
2
|
+
import type { AssetGuid as AssetGuidType, MaterialAsset, MeshAsset } from '@forgeax/engine-types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import {
|
|
5
|
+
materialAssetOutputProducer,
|
|
6
|
+
meshAssetOutputProducer,
|
|
7
|
+
} from '../scriptable-pack-output-producers.js';
|
|
8
|
+
|
|
9
|
+
function guid(value: string): AssetGuidType {
|
|
10
|
+
const parsed = AssetGuid.parse(value);
|
|
11
|
+
if (!parsed.ok) throw parsed.error;
|
|
12
|
+
return parsed.value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const MATERIAL_GUID = guid('019ffa97-3000-7000-8000-000000000001');
|
|
16
|
+
const PARENT_GUID = guid('019ffa97-3000-7000-8000-000000000002');
|
|
17
|
+
const TEXTURE_GUID = guid('019ffa97-3000-7000-8000-000000000003');
|
|
18
|
+
const SAMPLER_GUID = guid('019ffa97-3000-7000-8000-000000000004');
|
|
19
|
+
|
|
20
|
+
describe('ScriptablePack standard output producers', () => {
|
|
21
|
+
it('packs generated mesh bytes and derives default-material references', async () => {
|
|
22
|
+
const mesh: MeshAsset = {
|
|
23
|
+
kind: 'mesh',
|
|
24
|
+
vertices: new Float32Array([
|
|
25
|
+
0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0,
|
|
26
|
+
1, 1, 0, 0, 1,
|
|
27
|
+
]),
|
|
28
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
29
|
+
attributes: {
|
|
30
|
+
position: new Float32Array(9),
|
|
31
|
+
normal: new Float32Array(9),
|
|
32
|
+
uv: new Float32Array(6),
|
|
33
|
+
tangent: new Float32Array(12),
|
|
34
|
+
},
|
|
35
|
+
aabb: new Float32Array([0, 0, 0, 1, 0, 1]),
|
|
36
|
+
submeshes: [
|
|
37
|
+
{
|
|
38
|
+
topology: 'triangle-list',
|
|
39
|
+
indexOffset: 0,
|
|
40
|
+
indexCount: 3,
|
|
41
|
+
vertexCount: 3,
|
|
42
|
+
materialSlot: 0,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
materialSlots: [{ slotName: 'Shell', sourceKey: 'shell', defaultMaterial: MATERIAL_GUID }],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const result = await meshAssetOutputProducer.produce({
|
|
49
|
+
guid: '019ffa97-3000-7000-8000-000000000010',
|
|
50
|
+
sourceKey: 'geometry/shell',
|
|
51
|
+
asset: mesh,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result.ok).toBe(true);
|
|
55
|
+
if (!result.ok) return;
|
|
56
|
+
expect(result.value.payload).toBe(mesh);
|
|
57
|
+
expect(result.value.refs).toEqual([
|
|
58
|
+
{
|
|
59
|
+
guid: AssetGuid.format(MATERIAL_GUID),
|
|
60
|
+
sourceField: { fieldName: 'materialSlots', arrayIndex: 0 },
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
expect(result.value.artifacts.body).toMatchObject({
|
|
64
|
+
mediaType: 'application/x-forgeax-mesh',
|
|
65
|
+
assetCodec: { name: 'mesh-binary', version: '4' },
|
|
66
|
+
});
|
|
67
|
+
expect(result.value.artifacts.body?.bytes.byteLength).toBeGreaterThan(28);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('projects material GUID fields into one deterministic refs graph', async () => {
|
|
71
|
+
const material: MaterialAsset = {
|
|
72
|
+
kind: 'material',
|
|
73
|
+
parent: PARENT_GUID,
|
|
74
|
+
parameters: [{ name: 'surface', type: 'texture' }],
|
|
75
|
+
values: {
|
|
76
|
+
surface: {
|
|
77
|
+
texture: TEXTURE_GUID,
|
|
78
|
+
sampler: SAMPLER_GUID,
|
|
79
|
+
coordinates: { set: 1, transform: { scale: [2, 2] } },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const result = await materialAssetOutputProducer.produce({
|
|
85
|
+
guid: AssetGuid.format(MATERIAL_GUID),
|
|
86
|
+
sourceKey: 'material/shell',
|
|
87
|
+
asset: material,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result.ok).toBe(true);
|
|
91
|
+
if (!result.ok) return;
|
|
92
|
+
expect(result.value.refs).toEqual([
|
|
93
|
+
{ guid: AssetGuid.format(PARENT_GUID), sourceField: { fieldName: 'parent' } },
|
|
94
|
+
{
|
|
95
|
+
guid: AssetGuid.format(TEXTURE_GUID),
|
|
96
|
+
sourceField: { componentName: '<material>', fieldName: 'surface' },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
guid: AssetGuid.format(SAMPLER_GUID),
|
|
100
|
+
sourceField: { componentName: '<material>', fieldName: 'surface.sampler' },
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
expect(result.value.payload).toEqual({
|
|
104
|
+
...material,
|
|
105
|
+
parent: 0,
|
|
106
|
+
values: {
|
|
107
|
+
surface: {
|
|
108
|
+
texture: 1,
|
|
109
|
+
sampler: 2,
|
|
110
|
+
coordinates: { set: 1, transform: { scale: [2, 2] } },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
expect(result.value.artifacts).toEqual({});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('projects refs while keeping string texture authoring compact', async () => {
|
|
118
|
+
const material: MaterialAsset = {
|
|
119
|
+
kind: 'material',
|
|
120
|
+
parameters: [{ name: 'surface', type: 'texture' }],
|
|
121
|
+
values: { surface: AssetGuid.format(TEXTURE_GUID) },
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = await materialAssetOutputProducer.produce({
|
|
125
|
+
guid: AssetGuid.format(MATERIAL_GUID),
|
|
126
|
+
sourceKey: 'material/compact-surface',
|
|
127
|
+
asset: material,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.ok).toBe(true);
|
|
131
|
+
if (!result.ok) return;
|
|
132
|
+
expect(result.value.refs).toEqual([
|
|
133
|
+
{
|
|
134
|
+
guid: AssetGuid.format(TEXTURE_GUID),
|
|
135
|
+
sourceField: { componentName: '<material>', fieldName: 'surface' },
|
|
136
|
+
},
|
|
137
|
+
]);
|
|
138
|
+
const payload = result.value.payload as { values?: Record<string, unknown> };
|
|
139
|
+
expect(payload.values).toEqual({ surface: 0 });
|
|
140
|
+
expect(payload.values?.surface).not.toEqual({
|
|
141
|
+
texture: 0,
|
|
142
|
+
coordinates: { set: 0, transform: { offset: [0, 0], scale: [1, 1], rotation: 0 } },
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('projects a string texture shorthand as a ref when parameters are omitted', async () => {
|
|
147
|
+
const material: MaterialAsset = {
|
|
148
|
+
kind: 'material',
|
|
149
|
+
values: { baseColorTexture: AssetGuid.format(TEXTURE_GUID) },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const result = await materialAssetOutputProducer.produce({
|
|
153
|
+
guid: AssetGuid.format(MATERIAL_GUID),
|
|
154
|
+
sourceKey: 'material/compact-standard',
|
|
155
|
+
asset: material,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.ok).toBe(true);
|
|
159
|
+
if (!result.ok) return;
|
|
160
|
+
expect(result.value.refs).toEqual([
|
|
161
|
+
{
|
|
162
|
+
guid: AssetGuid.format(TEXTURE_GUID),
|
|
163
|
+
sourceField: { componentName: '<material>', fieldName: 'baseColorTexture' },
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
expect((result.value.payload as { values?: Record<string, unknown> }).values).toEqual({
|
|
167
|
+
baseColorTexture: 0,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('does not misclassify an ordinary string as a texture when parameters are omitted', async () => {
|
|
172
|
+
const material: MaterialAsset = {
|
|
173
|
+
kind: 'material',
|
|
174
|
+
values: { label: 'authoring-note' },
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const result = await materialAssetOutputProducer.produce({
|
|
178
|
+
guid: AssetGuid.format(MATERIAL_GUID),
|
|
179
|
+
sourceKey: 'material/compact-standard-string',
|
|
180
|
+
asset: material,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(result.ok).toBe(true);
|
|
184
|
+
if (!result.ok) return;
|
|
185
|
+
expect(result.value.refs).toEqual([]);
|
|
186
|
+
expect((result.value.payload as { values?: Record<string, unknown> }).values).toEqual({
|
|
187
|
+
label: 'authoring-note',
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|