@forgeax/engine-fbx 0.1.4 → 0.1.6

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.
@@ -1,251 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import {
3
- type AssetRegistry,
4
- createAssetRegistry,
5
- createCatalogSource,
6
- } from '@forgeax/engine-assets-runtime';
7
- import { meshAssetDecoder, meshAssetKind } from '@forgeax/engine-geometry';
8
- import { type DdcPack, normaliseForPack } from '@forgeax/engine-import';
9
- import type {
10
- AssetDecoder,
11
- AssetLoadError,
12
- MeshAsset,
13
- MorphTarget,
14
- Result,
15
- VertexAttributeMap,
16
- } from '@forgeax/engine-types';
17
- import { err } from '@forgeax/engine-types';
18
-
19
- const SCOPE_ID = 'fbx-test-scope';
20
- const GENERATION = 1;
21
- const PACK_DIGEST = `sha256:${'4'.repeat(64)}`;
22
- const OUTPUT_SET_DIGEST = `sha256:${'5'.repeat(64)}`;
23
- const OUTPUT_DIGEST = `sha256:${'6'.repeat(64)}`;
24
- const ATTRIBUTE_KEYS = [
25
- 'position',
26
- 'normal',
27
- 'uv',
28
- 'tangent',
29
- 'skinIndex',
30
- 'skinWeight',
31
- 'uv1',
32
- 'uv2',
33
- 'uv3',
34
- 'uv4',
35
- 'uv5',
36
- 'uv6',
37
- 'uv7',
38
- ] as const;
39
-
40
- type FixtureAsset = DdcPack['assets'][number];
41
-
42
- function sha256(bytes: Uint8Array): string {
43
- return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
44
- }
45
-
46
- function floatArray(value: unknown): Float32Array | undefined {
47
- if (value instanceof Float32Array) return value;
48
- if (value instanceof ArrayBuffer) return new Float32Array(value);
49
- if (ArrayBuffer.isView(value))
50
- return new Float32Array(Array.from(value as unknown as ArrayLike<number>));
51
- if (Array.isArray(value)) return new Float32Array(value as number[]);
52
- return undefined;
53
- }
54
-
55
- function indexArray(value: unknown): Uint16Array | Uint32Array | undefined {
56
- if (value instanceof Uint16Array || value instanceof Uint32Array) return value;
57
- if (ArrayBuffer.isView(value) || Array.isArray(value)) {
58
- const values = Array.from(value as unknown as ArrayLike<number>);
59
- return values.some((entry) => entry > 0xffff)
60
- ? new Uint32Array(values)
61
- : new Uint16Array(values);
62
- }
63
- return undefined;
64
- }
65
-
66
- function integerAttribute(value: unknown): Uint16Array | undefined {
67
- if (value instanceof Uint16Array) return value;
68
- if (Array.isArray(value) || ArrayBuffer.isView(value)) {
69
- return new Uint16Array(Array.from(value as unknown as ArrayLike<number>));
70
- }
71
- return undefined;
72
- }
73
-
74
- function morphTarget(value: unknown): MorphTarget | undefined {
75
- if (value === null || typeof value !== 'object') return undefined;
76
- const source = value as Record<string, unknown>;
77
- const position = floatArray(source.position);
78
- const normal = floatArray(source.normal);
79
- const tangent = floatArray(source.tangent);
80
- return {
81
- ...(position === undefined ? {} : { position }),
82
- ...(normal === undefined ? {} : { normal }),
83
- ...(tangent === undefined ? {} : { tangent }),
84
- };
85
- }
86
-
87
- function meshPayload(value: unknown): MeshAsset | undefined {
88
- if (value === null || typeof value !== 'object') return undefined;
89
- const source = value as Record<string, unknown>;
90
- const rawAttributes = source.attributes;
91
- if (
92
- source.kind !== 'mesh' ||
93
- floatArray(source.vertices) === undefined ||
94
- rawAttributes === null ||
95
- typeof rawAttributes !== 'object' ||
96
- !Array.isArray(source.submeshes) ||
97
- !Array.isArray(source.materialSlots)
98
- ) {
99
- return undefined;
100
- }
101
- const attributes: VertexAttributeMap = {};
102
- const sourceAttributes = rawAttributes as Record<string, unknown>;
103
- for (const key of ATTRIBUTE_KEYS) {
104
- const raw = sourceAttributes[key];
105
- if (raw === undefined) continue;
106
- const converted = key === 'skinIndex' ? integerAttribute(raw) : floatArray(raw);
107
- if (converted !== undefined) attributes[key] = converted;
108
- }
109
- const morphWeights = floatArray(source.morphWeights);
110
- const morphTargets = Array.isArray(source.morphTargets)
111
- ? source.morphTargets.flatMap((target) => {
112
- const converted = morphTarget(target);
113
- return converted === undefined ? [] : [converted];
114
- })
115
- : undefined;
116
- const vertices = floatArray(source.vertices) as Float32Array;
117
- const indices = indexArray(source.indices);
118
- const aabb = floatArray(source.aabb);
119
- return {
120
- kind: 'mesh',
121
- vertices,
122
- ...(indices === undefined ? {} : { indices }),
123
- attributes,
124
- ...(aabb === undefined ? {} : { aabb }),
125
- submeshes: source.submeshes as MeshAsset['submeshes'],
126
- materialSlots: source.materialSlots as MeshAsset['materialSlots'],
127
- ...(morphTargets === undefined ? {} : { morphTargets }),
128
- ...(morphWeights === undefined ? {} : { morphWeights }),
129
- };
130
- }
131
-
132
- export const normalizedMeshAssetDecoder: AssetDecoder<MeshAsset> = {
133
- async decode(input): Promise<Result<MeshAsset, AssetLoadError>> {
134
- const body = input.envelope.artifacts.body;
135
- if (body !== undefined) {
136
- const bytes = await input.artifacts.read(body);
137
- if (!bytes.ok) return bytes;
138
- }
139
- const payload = meshPayload(input.envelope.payload);
140
- if (payload === undefined) {
141
- return err({
142
- code: 'asset-package-invalid',
143
- expected: 'a JSON-safe mesh payload that can be owned by Geometry',
144
- hint: 'recook the mesh payload with typed vertex and attribute fields',
145
- detail: { guid: input.envelope.guid, reason: 'mesh payload normalization failed' },
146
- });
147
- }
148
- return meshAssetDecoder.decode({
149
- ...input,
150
- envelope: { ...input.envelope, payload },
151
- });
152
- },
153
- };
154
-
155
- export function installMeshDecoder(assets: AssetRegistry) {
156
- return assets.installDecoder(meshAssetKind, normalizedMeshAssetDecoder);
157
- }
158
-
159
- function row(asset: FixtureAsset, packageUrl: string) {
160
- const sourcePath = `fixtures/${asset.guid}`;
161
- return {
162
- guid: asset.guid,
163
- packageUrl,
164
- kind: asset.kind,
165
- sourcePath,
166
- revision: { rootId: 'fbx-fixture', digest: 'sha256:catalog', observedAt: 1 },
167
- publication: {
168
- schemaVersion: 'asset-publication/1' as const,
169
- sourcePath,
170
- sourceRevision: 'fbx-fixture-source',
171
- generation: GENERATION,
172
- digest: PACK_DIGEST,
173
- outputSetDigest: OUTPUT_SET_DIGEST,
174
- outputs: [
175
- {
176
- guid: asset.guid,
177
- sourceKey: asset.sourceKey ?? asset.guid,
178
- kind: asset.kind,
179
- digest: OUTPUT_DIGEST,
180
- refs: asset.refs,
181
- },
182
- ],
183
- receipt: {
184
- schemaVersion: 'asset-publication-receipt/1' as const,
185
- sourcePath,
186
- sourceRevision: 'fbx-fixture-source',
187
- inputFingerprint: 'fbx-fixture-input',
188
- outputDigest: OUTPUT_DIGEST,
189
- outputSetDigest: OUTPUT_SET_DIGEST,
190
- externalEvidence: [],
191
- },
192
- externalEvidence: [],
193
- },
194
- };
195
- }
196
-
197
- export function createAssetRuntimeFixture(sourceAssets: readonly FixtureAsset[]): AssetRegistry {
198
- const packageUrl = 'https://assets.test/fbx-fixture.pack.json';
199
- const artifactBytes = new Map<string, Uint8Array>();
200
- const runtimeAssets = sourceAssets.map((asset) => {
201
- const artifacts = Object.fromEntries(
202
- Object.entries(asset.artifacts).map(([key, body]) => {
203
- const path = `${asset.guid}/${key}.bin`;
204
- const url = new URL(path, packageUrl).toString();
205
- artifactBytes.set(url, body.bytes);
206
- return [
207
- key,
208
- {
209
- path,
210
- mediaType: body.mediaType,
211
- contentEncoding: 'identity' as const,
212
- byteLength: body.bytes.byteLength,
213
- integrity: { algorithm: 'sha256' as const, digest: sha256(body.bytes) },
214
- ...(body.assetCodec === undefined ? {} : { assetCodec: body.assetCodec }),
215
- },
216
- ];
217
- }),
218
- );
219
- return {
220
- guid: asset.guid,
221
- kind: asset.kind,
222
- ...(asset.name === undefined ? {} : { name: asset.name }),
223
- payload: normaliseForPack(asset.payload) as Record<string, unknown>,
224
- refs: asset.refs,
225
- artifacts,
226
- };
227
- });
228
- const pack = {
229
- schemaVersion: '2.0.0' as const,
230
- kind: 'internal-text-package' as const,
231
- scopeId: SCOPE_ID,
232
- generation: GENERATION,
233
- digest: PACK_DIGEST,
234
- outputSetDigest: OUTPUT_SET_DIGEST,
235
- assets: runtimeAssets,
236
- };
237
- const fetcher: typeof globalThis.fetch = async (input) => {
238
- const url = String(input);
239
- if (url === packageUrl) return new Response(JSON.stringify(pack));
240
- const bytes = artifactBytes.get(url);
241
- return bytes === undefined
242
- ? new Response('not found', { status: 404 })
243
- : new Response(bytes as unknown as BodyInit);
244
- };
245
- return createAssetRegistry({
246
- catalog: createCatalogSource({ entries: sourceAssets.map((asset) => row(asset, packageUrl)) }),
247
- fetcher,
248
- scopeId: SCOPE_ID,
249
- generation: GENERATION,
250
- });
251
- }