@aura3d/assets 1.4.0 → 1.4.1

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.
Files changed (69) hide show
  1. package/package.json +38 -5
  2. package/AGENTS.md +0 -46
  3. package/src/AdvancedAssetCorpus.ts +0 -454
  4. package/src/AssetBundleCacheFixtures.ts +0 -290
  5. package/src/AssetCache.ts +0 -76
  6. package/src/AssetCompatibility.ts +0 -224
  7. package/src/AssetCorpus.ts +0 -329
  8. package/src/AssetDependencyGraph.ts +0 -83
  9. package/src/AssetHandle.ts +0 -74
  10. package/src/AssetImportPreflight.ts +0 -179
  11. package/src/AssetInspection.ts +0 -706
  12. package/src/AssetLoader.ts +0 -36
  13. package/src/AssetManager.ts +0 -211
  14. package/src/AssetRegistry.ts +0 -44
  15. package/src/AssetRenderDefaults.ts +0 -36
  16. package/src/AudioLoader.ts +0 -41
  17. package/src/BlenderExportValidation.ts +0 -227
  18. package/src/CarConceptMaterialStability.ts +0 -1405
  19. package/src/EXRLoader.ts +0 -1
  20. package/src/ExternalParityGLTFCorpus.ts +0 -65
  21. package/src/GLTFAnimationRuntime.ts +0 -1416
  22. package/src/GLTFAutoFit.ts +0 -133
  23. package/src/GLTFCompressionDecoders.ts +0 -190
  24. package/src/GLTFExtensionSupport.ts +0 -138
  25. package/src/GLTFLoader.ts +0 -4060
  26. package/src/GLTFRenderResources.ts +0 -2270
  27. package/src/HDRLoader.ts +0 -1
  28. package/src/ImageLoader.ts +0 -52
  29. package/src/ImportPipeline.ts +0 -148
  30. package/src/KTX2BasisTextureTranscoder.ts +0 -225
  31. package/src/LoadContext.ts +0 -60
  32. package/src/MaterialLoader.ts +0 -166
  33. package/src/MeshOptimization.ts +0 -93
  34. package/src/OBJLoader.ts +0 -551
  35. package/src/SceneAnalysisFixtures.ts +0 -438
  36. package/src/SceneLoader.ts +0 -62
  37. package/src/ShaderLoader.ts +0 -30
  38. package/src/TextureLoader.ts +0 -30
  39. package/src/TexturePipeline.ts +0 -96
  40. package/src/TextureStreaming.ts +0 -20
  41. package/src/WorkerAssetJobs.ts +0 -56
  42. package/src/advanced-gallery/index.ts +0 -18
  43. package/src/asset-corpus/GLTFSceneLoader.ts +0 -9
  44. package/src/asset-corpus/ProductionAssetCorpus.ts +0 -230
  45. package/src/asset-corpus/ProductionAssetPipeline.ts +0 -5
  46. package/src/asset-corpus/ProductionGLTFRenderPipeline.ts +0 -182
  47. package/src/asset-corpus/ProductionHDRTextureLoader.ts +0 -2
  48. package/src/asset-corpus/ProductionKTX2TextureLoader.ts +0 -2
  49. package/src/asset-corpus/ProductionTextureLoader.ts +0 -2
  50. package/src/asset-corpus/index.ts +0 -29
  51. package/src/browser-index.ts +0 -229
  52. package/src/createRenderableScene.ts +0 -128
  53. package/src/index.ts +0 -325
  54. package/src/loadRenderableAsset.ts +0 -67
  55. package/src/loaders/CubeTextureLoader.ts +0 -8
  56. package/src/loaders/EXRLoader.ts +0 -10
  57. package/src/loaders/HDRLoader.ts +0 -7
  58. package/src/loaders/KTX2Loader.ts +0 -7
  59. package/src/loaders/LoaderDiagnostics.ts +0 -36
  60. package/src/loaders/MTLLoader.ts +0 -20
  61. package/src/loaders/OBJLoader.ts +0 -24
  62. package/src/loaders/TextureLoader.ts +0 -14
  63. package/src/loaders/ThreeCompatGLTFLoader.ts +0 -19
  64. package/src/loaders/index.ts +0 -14
  65. package/src/threejs-compatibility/ThreeCompatAssetProvenance.ts +0 -51
  66. package/src/threejs-compatibility/ThreeCompatAssetRegistry.ts +0 -61
  67. package/src/threejs-example-parity/index.ts +0 -262
  68. package/tests/assets.test.ts +0 -393
  69. package/tsconfig.json +0 -7
@@ -1,93 +0,0 @@
1
- import type { ImportStage } from "./ImportPipeline";
2
-
3
- export type MeshAttributeValue = number | readonly number[];
4
-
5
- export interface MeshOptimizationInput {
6
- readonly vertexCount: number;
7
- readonly indices: readonly number[];
8
- readonly attributes?: Readonly<Record<string, readonly MeshAttributeValue[]>>;
9
- }
10
-
11
- export interface MeshOptimizationResult {
12
- readonly vertexCount: number;
13
- readonly indices: readonly number[];
14
- readonly attributes: Readonly<Record<string, readonly MeshAttributeValue[]>>;
15
- readonly remap: readonly number[];
16
- readonly removedVertices: number;
17
- }
18
-
19
- export interface MeshOptimizationStageOptions {
20
- readonly name?: string;
21
- }
22
-
23
- export function optimizeIndexedMesh(input: MeshOptimizationInput): MeshOptimizationResult {
24
- validateMeshOptimizationInput(input);
25
-
26
- const remap = new Array<number>(input.vertexCount).fill(-1);
27
- const usedVertices: number[] = [];
28
- const optimizedIndices = input.indices.map((index) => {
29
- let nextIndex = remap[index];
30
- if (nextIndex === -1) {
31
- nextIndex = usedVertices.length;
32
- remap[index] = nextIndex;
33
- usedVertices.push(index);
34
- }
35
- return nextIndex;
36
- });
37
-
38
- const attributes: Record<string, readonly MeshAttributeValue[]> = {};
39
- for (const [semantic, values] of Object.entries(input.attributes ?? {})) {
40
- attributes[semantic] = usedVertices.map((sourceIndex) => cloneAttributeValue(values[sourceIndex]!));
41
- }
42
-
43
- return {
44
- vertexCount: usedVertices.length,
45
- indices: optimizedIndices,
46
- attributes,
47
- remap,
48
- removedVertices: input.vertexCount - usedVertices.length
49
- };
50
- }
51
-
52
- export function createMeshOptimizationStage(options: MeshOptimizationStageOptions = {}): ImportStage<MeshOptimizationInput, MeshOptimizationResult> {
53
- return {
54
- name: options.name ?? "mesh-optimization",
55
- run: (input) => optimizeIndexedMesh(input)
56
- };
57
- }
58
-
59
- function validateMeshOptimizationInput(input: MeshOptimizationInput): void {
60
- if (!Number.isInteger(input.vertexCount) || input.vertexCount < 0) {
61
- throw new Error("Mesh optimization vertexCount must be a non-negative integer");
62
- }
63
- if (!Array.isArray(input.indices) || input.indices.length === 0) {
64
- throw new Error("Mesh optimization requires one or more indices");
65
- }
66
- input.indices.forEach((index, offset) => {
67
- if (!Number.isInteger(index) || index < 0 || index >= input.vertexCount) {
68
- throw new Error(`Mesh optimization index ${offset} references invalid vertex ${index}`);
69
- }
70
- });
71
- for (const [semantic, values] of Object.entries(input.attributes ?? {})) {
72
- if (!Array.isArray(values) || values.length !== input.vertexCount) {
73
- throw new Error(`Mesh optimization attribute ${semantic} must contain exactly ${input.vertexCount} values`);
74
- }
75
- values.forEach((value, index) => validateAttributeValue(value, semantic, index));
76
- }
77
- }
78
-
79
- function validateAttributeValue(value: MeshAttributeValue, semantic: string, index: number): void {
80
- if (typeof value === "number") {
81
- if (!Number.isFinite(value)) {
82
- throw new Error(`Mesh optimization attribute ${semantic}[${index}] must be finite`);
83
- }
84
- return;
85
- }
86
- if (!Array.isArray(value) || value.length === 0 || value.some((component) => !Number.isFinite(component))) {
87
- throw new Error(`Mesh optimization attribute ${semantic}[${index}] must be a finite scalar or numeric tuple`);
88
- }
89
- }
90
-
91
- function cloneAttributeValue(value: MeshAttributeValue): MeshAttributeValue {
92
- return Array.isArray(value) ? [...value] : value;
93
- }
package/src/OBJLoader.ts DELETED
@@ -1,551 +0,0 @@
1
- import type { AssetLoadProgress, AssetLoadRequest, AssetLoader } from "./AssetLoader";
2
- import { GLTFLoader, type GLTFAsset, type GLTFLoaderDiagnostics } from "./GLTFLoader";
3
- import type { LoadContext } from "./LoadContext";
4
-
5
- type Vec2 = readonly [number, number];
6
- type Vec3 = readonly [number, number, number];
7
-
8
- interface OBJFaceRef {
9
- readonly position: number;
10
- readonly texcoord?: number;
11
- readonly normal?: number;
12
- }
13
-
14
- interface ParsedOBJ {
15
- readonly name: string;
16
- readonly materialLibraries: readonly string[];
17
- readonly materialNames: readonly string[];
18
- readonly triangleMaterials: readonly string[];
19
- readonly positions: readonly Vec3[];
20
- readonly normals: readonly Vec3[];
21
- readonly texcoords: readonly Vec2[];
22
- readonly indices: readonly number[];
23
- readonly min: Vec3;
24
- readonly max: Vec3;
25
- readonly source: {
26
- readonly vertexCount: number;
27
- readonly normalCount: number;
28
- readonly texcoordCount: number;
29
- readonly faceCount: number;
30
- readonly triangulatedFaceCount: number;
31
- readonly generatedNormals: boolean;
32
- };
33
- }
34
-
35
- interface OBJMaterialDefinition {
36
- readonly name: string;
37
- readonly baseColorFactor: readonly [number, number, number, number];
38
- readonly metallicFactor: number;
39
- readonly roughnessFactor: number;
40
- readonly alphaMode: "OPAQUE" | "BLEND";
41
- }
42
-
43
- interface OBJLoadRequest extends AssetLoadRequest {
44
- readonly mtl?: string;
45
- readonly materialLibraries?: Readonly<Record<string, string>>;
46
- }
47
-
48
- interface WrappedOBJAsset extends GLTFAsset {
49
- readonly __objInnerAsset?: GLTFAsset;
50
- }
51
-
52
- export class OBJLoader implements AssetLoader<GLTFAsset> {
53
- readonly type = "obj";
54
-
55
- private readonly gltfLoader: GLTFLoader;
56
- private readonly innerAssets = new WeakMap<GLTFAsset, GLTFAsset>();
57
-
58
- constructor(gltfLoader = new GLTFLoader()) {
59
- this.gltfLoader = gltfLoader;
60
- }
61
-
62
- canLoad(request: AssetLoadRequest): boolean {
63
- return request.type === "obj" || /\.obj(?:[?#]|$)/i.test(request.url) || request.url.startsWith("data:model/obj") || request.url.startsWith("data:text/plain");
64
- }
65
-
66
- async load(request: AssetLoadRequest, context: LoadContext): Promise<GLTFAsset> {
67
- context.throwIfAborted(request.url);
68
- const source = await loadOBJText(request, context);
69
- context.throwIfAborted(request.url);
70
- const parsed = parseOBJ(source, objNameFromUrl(request.url));
71
- const materials = await loadOBJMaterialDefinitions(request as OBJLoadRequest, parsed, context);
72
- const gltfUrl = createGLTFDataUrl(parsed, materials);
73
- const inner = await this.gltfLoader.load({ url: gltfUrl, type: "gltf", signal: request.signal, onProgress: request.onProgress }, context);
74
- const wrapped = wrapOBJAsset(request.url, inner, parsed);
75
- this.innerAssets.set(wrapped, inner);
76
- return wrapped;
77
- }
78
-
79
- dispose(asset: GLTFAsset): void {
80
- this.gltfLoader.dispose(this.innerAssets.get(asset) ?? asset);
81
- }
82
- }
83
-
84
- function wrapOBJAsset(url: string, inner: GLTFAsset, parsed: ParsedOBJ): GLTFAsset {
85
- const loaderDiagnostics = augmentDiagnostics(inner.loaderDiagnostics, parsed);
86
- const wrapped: WrappedOBJAsset = {
87
- url,
88
- get disposed() {
89
- return inner.disposed;
90
- },
91
- loaderDiagnostics,
92
- images: inner.images,
93
- textures: inner.textures,
94
- materials: inner.materials,
95
- materialVariants: inner.materialVariants,
96
- scenes: inner.scenes,
97
- defaultScene: inner.defaultScene,
98
- meshes: inner.meshes,
99
- cameras: inner.cameras,
100
- lights: inner.lights,
101
- skins: inner.skins,
102
- animations: inner.animations,
103
- createScene: (options) => inner.createScene(options),
104
- toJSON: () => ({
105
- ...inner.toJSON(),
106
- url,
107
- loaderDiagnostics
108
- }),
109
- __objInnerAsset: inner
110
- };
111
- return wrapped;
112
- }
113
-
114
- function augmentDiagnostics(diagnostics: GLTFLoaderDiagnostics, parsed: ParsedOBJ): GLTFLoaderDiagnostics {
115
- const features = new Set(diagnostics.features);
116
- features.add("obj");
117
- features.add("obj-native-import");
118
- features.add("obj-triangulated-faces");
119
- if (parsed.source.generatedNormals) features.add("obj-generated-normals");
120
- if (parsed.source.texcoordCount > 0) features.add("obj-texcoords");
121
- if (parsed.source.normalCount > 0) features.add("obj-normals");
122
- if (parsed.materialLibraries.length > 0) features.add("obj-mtllib");
123
- if (parsed.materialNames.length > 0) features.add("obj-usemtl");
124
- if (parsed.materialNames.length > 1) features.add("obj-multi-material");
125
- return {
126
- ...diagnostics,
127
- features: [...features].sort()
128
- };
129
- }
130
-
131
- async function loadOBJText(request: AssetLoadRequest, context: LoadContext): Promise<string> {
132
- if (request.url.startsWith("data:")) {
133
- const bytes = decodeOBJDataUri(request.url);
134
- reportProgress(request, "document", bytes.byteLength, bytes.byteLength);
135
- return new TextDecoder().decode(bytes);
136
- }
137
- if (typeof fetch !== "function") {
138
- throw new Error("OBJLoader requires fetch for non-data URLs");
139
- }
140
- const response = await fetch(request.url, { signal: request.signal });
141
- if (!response.ok) {
142
- throw new Error(`OBJ request failed with ${response.status}`);
143
- }
144
- const bytes = new Uint8Array(await response.arrayBuffer());
145
- context.throwIfAborted(request.url);
146
- reportProgress(request, "document", bytes.byteLength, bytes.byteLength);
147
- return new TextDecoder().decode(bytes);
148
- }
149
-
150
- function reportProgress(request: AssetLoadRequest, phase: AssetLoadProgress["phase"], loadedBytes: number, totalBytes?: number): void {
151
- request.onProgress?.({ url: request.url, phase, loadedBytes, totalBytes });
152
- }
153
-
154
- function parseOBJ(source: string, name: string): ParsedOBJ {
155
- const sourcePositions: Vec3[] = [];
156
- const sourceNormals: Vec3[] = [];
157
- const sourceTexcoords: Vec2[] = [];
158
- const positions: Vec3[] = [];
159
- const normals: Vec3[] = [];
160
- const texcoords: Vec2[] = [];
161
- const indices: number[] = [];
162
- const materialLibraries: string[] = [];
163
- const materialNames = new Set<string>();
164
- const triangleMaterials: string[] = [];
165
- const min: [number, number, number] = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
166
- const max: [number, number, number] = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
167
- let faceCount = 0;
168
- let generatedNormals = false;
169
- let activeMaterial = "obj-default-pbr-material";
170
-
171
- for (const [lineIndex, rawLine] of source.split(/\r?\n/).entries()) {
172
- const line = rawLine.split("#", 1)[0]?.trim() ?? "";
173
- if (!line) continue;
174
- const [keyword, ...fields] = line.split(/\s+/);
175
- if (keyword === "v") {
176
- sourcePositions.push(parseVec3(fields, `OBJ line ${lineIndex + 1} vertex`));
177
- } else if (keyword === "vn") {
178
- sourceNormals.push(normalizeVec3(parseVec3(fields, `OBJ line ${lineIndex + 1} normal`)));
179
- } else if (keyword === "vt") {
180
- sourceTexcoords.push(parseVec2(fields, `OBJ line ${lineIndex + 1} texcoord`));
181
- } else if (keyword === "mtllib") {
182
- if (fields.length === 0) throw new Error(`OBJ line ${lineIndex + 1} mtllib must reference a material library`);
183
- materialLibraries.push(fields.join(" "));
184
- } else if (keyword === "usemtl") {
185
- if (fields.length === 0) throw new Error(`OBJ line ${lineIndex + 1} usemtl must reference a material name`);
186
- activeMaterial = fields.join(" ");
187
- materialNames.add(activeMaterial);
188
- } else if (keyword === "f") {
189
- if (fields.length < 3) throw new Error(`OBJ line ${lineIndex + 1} face must have at least 3 vertices`);
190
- const refs = fields.map((field) => parseFaceRef(field, sourcePositions.length, sourceTexcoords.length, sourceNormals.length, lineIndex + 1));
191
- for (let index = 1; index < refs.length - 1; index += 1) {
192
- const triangle = [refs[0]!, refs[index]!, refs[index + 1]!];
193
- const fallbackNormal = faceNormal(
194
- sourcePositions[triangle[0].position]!,
195
- sourcePositions[triangle[1].position]!,
196
- sourcePositions[triangle[2].position]!
197
- );
198
- for (const ref of triangle) {
199
- const position = sourcePositions[ref.position]!;
200
- const normal = ref.normal === undefined ? fallbackNormal : sourceNormals[ref.normal]!;
201
- const texcoord = ref.texcoord === undefined ? [0, 0] as const : sourceTexcoords[ref.texcoord]!;
202
- if (ref.normal === undefined) generatedNormals = true;
203
- positions.push(position);
204
- normals.push(normal);
205
- texcoords.push(texcoord);
206
- indices.push(indices.length);
207
- updateBounds(min, max, position);
208
- }
209
- triangleMaterials.push(activeMaterial);
210
- }
211
- faceCount += 1;
212
- }
213
- }
214
-
215
- if (sourcePositions.length === 0) throw new Error("OBJ asset must contain at least one vertex.");
216
- if (indices.length === 0) throw new Error("OBJ asset must contain at least one triangulatable face.");
217
- return {
218
- name,
219
- materialLibraries,
220
- materialNames: [...materialNames],
221
- triangleMaterials,
222
- positions,
223
- normals,
224
- texcoords: sourceTexcoords.length > 0 ? texcoords : [],
225
- indices,
226
- min,
227
- max,
228
- source: {
229
- vertexCount: sourcePositions.length,
230
- normalCount: sourceNormals.length,
231
- texcoordCount: sourceTexcoords.length,
232
- faceCount,
233
- triangulatedFaceCount: indices.length / 3,
234
- generatedNormals
235
- }
236
- };
237
- }
238
-
239
- async function loadOBJMaterialDefinitions(request: OBJLoadRequest, parsed: ParsedOBJ, context: LoadContext): Promise<ReadonlyMap<string, OBJMaterialDefinition>> {
240
- const materials = new Map<string, OBJMaterialDefinition>();
241
- const inline = request.mtl;
242
- if (inline) {
243
- mergeMaterials(materials, parseMTL(inline));
244
- }
245
- for (const library of parsed.materialLibraries) {
246
- const librarySource = request.materialLibraries?.[library];
247
- if (librarySource !== undefined) {
248
- mergeMaterials(materials, parseMTL(librarySource));
249
- continue;
250
- }
251
- if (request.url.startsWith("data:")) continue;
252
- if (typeof fetch !== "function") continue;
253
- const url = new URL(library, request.url).toString();
254
- const response = await fetch(url, { signal: request.signal });
255
- if (!response.ok) continue;
256
- context.throwIfAborted(url);
257
- mergeMaterials(materials, parseMTL(await response.text()));
258
- }
259
- return materials;
260
- }
261
-
262
- function mergeMaterials(target: Map<string, OBJMaterialDefinition>, source: ReadonlyMap<string, OBJMaterialDefinition>): void {
263
- for (const [name, material] of source) target.set(name, material);
264
- }
265
-
266
- function parseMTL(source: string): ReadonlyMap<string, OBJMaterialDefinition> {
267
- const materials = new Map<string, OBJMaterialDefinition>();
268
- let current: {
269
- name: string;
270
- kd: [number, number, number];
271
- alpha: number;
272
- roughness: number;
273
- } | undefined;
274
- const flush = (): void => {
275
- if (!current) return;
276
- materials.set(current.name, {
277
- name: current.name,
278
- baseColorFactor: [current.kd[0], current.kd[1], current.kd[2], current.alpha],
279
- metallicFactor: 0,
280
- roughnessFactor: current.roughness,
281
- alphaMode: current.alpha < 0.999 ? "BLEND" : "OPAQUE"
282
- });
283
- };
284
- for (const [lineIndex, rawLine] of source.split(/\r?\n/).entries()) {
285
- const line = rawLine.split("#", 1)[0]?.trim() ?? "";
286
- if (!line) continue;
287
- const [keyword, ...fields] = line.split(/\s+/);
288
- if (keyword === "newmtl") {
289
- flush();
290
- if (fields.length === 0) throw new Error(`MTL line ${lineIndex + 1} newmtl must include a material name`);
291
- current = { name: fields.join(" "), kd: [0.72, 0.72, 0.68], alpha: 1, roughness: 0.62 };
292
- } else if (current && keyword === "Kd") {
293
- current.kd = parseColor3(fields, `MTL line ${lineIndex + 1} Kd`);
294
- } else if (current && keyword === "d") {
295
- current.alpha = clamp01(parseFinite(fields[0], `MTL line ${lineIndex + 1} d`));
296
- } else if (current && keyword === "Tr") {
297
- current.alpha = 1 - clamp01(parseFinite(fields[0], `MTL line ${lineIndex + 1} Tr`));
298
- } else if (current && keyword === "Ns") {
299
- const shininess = Math.max(0, parseFinite(fields[0], `MTL line ${lineIndex + 1} Ns`));
300
- current.roughness = clamp01(Math.sqrt(2 / (shininess + 2)));
301
- }
302
- }
303
- flush();
304
- return materials;
305
- }
306
-
307
- function createGLTFDataUrl(parsed: ParsedOBJ, materialDefinitions: ReadonlyMap<string, OBJMaterialDefinition> = new Map()): string {
308
- const buffer = new BinaryBufferBuilder();
309
- const positionView = buffer.appendFloat32(parsed.positions.flatMap((position) => [...position]));
310
- const normalView = buffer.appendFloat32(parsed.normals.flatMap((normal) => [...normal]));
311
- const texcoordView = parsed.texcoords.length > 0
312
- ? buffer.appendFloat32(parsed.texcoords.flatMap((texcoord) => [...texcoord]))
313
- : undefined;
314
- const materialNames = parsed.materialNames.length > 0 ? parsed.materialNames : ["obj-default-pbr-material"];
315
- const materialIndexByName = new Map(materialNames.map((name, index) => [name, index] as const));
316
- const indicesUseUint32 = parsed.positions.length > 65_535;
317
- const attributes: Record<string, number> = { POSITION: 0, NORMAL: 1 };
318
- const accessors: unknown[] = [
319
- { bufferView: positionView, componentType: 5126, count: parsed.positions.length, type: "VEC3", min: parsed.min, max: parsed.max },
320
- { bufferView: normalView, componentType: 5126, count: parsed.normals.length, type: "VEC3" }
321
- ];
322
- if (texcoordView !== undefined) {
323
- attributes.TEXCOORD_0 = accessors.length;
324
- accessors.push({ bufferView: texcoordView, componentType: 5126, count: parsed.texcoords.length, type: "VEC2" });
325
- }
326
- const primitives = materialNames.map((name, materialIndex) => {
327
- const indices = indicesForMaterial(parsed, materialIndexByName, materialIndex, name);
328
- const indexView = indicesUseUint32 ? buffer.appendUint32(indices) : buffer.appendUint16(indices);
329
- const indexAccessor = accessors.length;
330
- accessors.push({ bufferView: indexView, componentType: indicesUseUint32 ? 5125 : 5123, count: indices.length, type: "SCALAR" });
331
- return { attributes, indices: indexAccessor, material: materialIndex };
332
- }).filter((primitive) => {
333
- const accessor = accessors[primitive.indices] as { readonly count: number };
334
- return accessor.count > 0;
335
- });
336
- const bytes = buffer.bytes();
337
- const gltf = {
338
- asset: { version: "2.0", generator: "Aura3D OBJLoader geometry and MTL importer" },
339
- buffers: [{ uri: `data:application/octet-stream;base64,${bytesToBase64(bytes)}`, byteLength: bytes.byteLength }],
340
- bufferViews: buffer.views,
341
- accessors,
342
- materials: materialNames.map((name) => gltfMaterialFromOBJ(name, materialDefinitions.get(name))),
343
- meshes: [{
344
- name: parsed.name,
345
- primitives
346
- }],
347
- nodes: [{ name: `${parsed.name}-node`, mesh: 0 }],
348
- scenes: [{ name: `${parsed.name}-scene`, nodes: [0] }],
349
- scene: 0
350
- };
351
- return `data:model/gltf+json,${encodeURIComponent(JSON.stringify(gltf))}`;
352
- }
353
-
354
- function indicesForMaterial(
355
- parsed: ParsedOBJ,
356
- materialIndexByName: ReadonlyMap<string, number>,
357
- materialIndex: number,
358
- materialName: string
359
- ): readonly number[] {
360
- const indices: number[] = [];
361
- for (let triangle = 0; triangle < parsed.triangleMaterials.length; triangle += 1) {
362
- const name = parsed.triangleMaterials[triangle] ?? "obj-default-pbr-material";
363
- const index = materialIndexByName.get(name) ?? materialIndexByName.get(materialName);
364
- if (index !== materialIndex) continue;
365
- const offset = triangle * 3;
366
- indices.push(parsed.indices[offset]!, parsed.indices[offset + 1]!, parsed.indices[offset + 2]!);
367
- }
368
- return indices;
369
- }
370
-
371
- function gltfMaterialFromOBJ(name: string, material: OBJMaterialDefinition | undefined): unknown {
372
- const resolved = material ?? {
373
- name,
374
- baseColorFactor: [0.72, 0.72, 0.68, 1] as const,
375
- metallicFactor: 0,
376
- roughnessFactor: 0.62,
377
- alphaMode: "OPAQUE" as const
378
- };
379
- return {
380
- name: resolved.name,
381
- alphaMode: resolved.alphaMode,
382
- pbrMetallicRoughness: {
383
- baseColorFactor: resolved.baseColorFactor,
384
- metallicFactor: resolved.metallicFactor,
385
- roughnessFactor: resolved.roughnessFactor
386
- }
387
- };
388
- }
389
-
390
- class BinaryBufferBuilder {
391
- readonly views: Array<{ readonly buffer: 0; readonly byteOffset: number; readonly byteLength: number }> = [];
392
- private readonly chunks: Uint8Array[] = [];
393
- private byteLength = 0;
394
-
395
- appendFloat32(values: readonly number[]): number {
396
- const data = new Float32Array(values);
397
- return this.appendBytes(new Uint8Array(data.buffer));
398
- }
399
-
400
- appendUint16(values: readonly number[]): number {
401
- const data = new Uint16Array(values);
402
- return this.appendBytes(new Uint8Array(data.buffer));
403
- }
404
-
405
- appendUint32(values: readonly number[]): number {
406
- const data = new Uint32Array(values);
407
- return this.appendBytes(new Uint8Array(data.buffer));
408
- }
409
-
410
- appendBytes(bytes: Uint8Array): number {
411
- this.pad4();
412
- const viewIndex = this.views.length;
413
- this.views.push({ buffer: 0, byteOffset: this.byteLength, byteLength: bytes.byteLength });
414
- this.chunks.push(bytes);
415
- this.byteLength += bytes.byteLength;
416
- return viewIndex;
417
- }
418
-
419
- bytes(): Uint8Array {
420
- this.pad4();
421
- const output = new Uint8Array(this.byteLength);
422
- let offset = 0;
423
- for (const chunk of this.chunks) {
424
- output.set(chunk, offset);
425
- offset += chunk.byteLength;
426
- }
427
- return output;
428
- }
429
-
430
- private pad4(): void {
431
- const padding = (4 - (this.byteLength % 4)) % 4;
432
- if (padding === 0) return;
433
- this.chunks.push(new Uint8Array(padding));
434
- this.byteLength += padding;
435
- }
436
- }
437
-
438
- function parseFaceRef(field: string, positionCount: number, texcoordCount: number, normalCount: number, line: number): OBJFaceRef {
439
- const parts = field.split("/");
440
- const position = resolveOBJIndex(parts[0], positionCount, `OBJ line ${line} face position`);
441
- const texcoord = parts[1] ? resolveOBJIndex(parts[1], texcoordCount, `OBJ line ${line} face texcoord`) : undefined;
442
- const normal = parts[2] ? resolveOBJIndex(parts[2], normalCount, `OBJ line ${line} face normal`) : undefined;
443
- return {
444
- position,
445
- ...(texcoord !== undefined ? { texcoord } : {}),
446
- ...(normal !== undefined ? { normal } : {})
447
- };
448
- }
449
-
450
- function resolveOBJIndex(value: string | undefined, count: number, label: string): number {
451
- const parsed = Number(value);
452
- if (!Number.isInteger(parsed) || parsed === 0) throw new Error(`${label} index must be a non-zero integer.`);
453
- const index = parsed > 0 ? parsed - 1 : count + parsed;
454
- if (index < 0 || index >= count) throw new RangeError(`${label} index ${parsed} is outside available count ${count}.`);
455
- return index;
456
- }
457
-
458
- function parseVec3(fields: readonly string[], label: string): Vec3 {
459
- if (fields.length < 3) throw new Error(`${label} must contain three numeric components.`);
460
- const value = [Number(fields[0]), Number(fields[1]), Number(fields[2])] as const;
461
- if (value.some((component) => !Number.isFinite(component))) throw new Error(`${label} must contain finite numeric components.`);
462
- return value;
463
- }
464
-
465
- function parseVec2(fields: readonly string[], label: string): Vec2 {
466
- if (fields.length < 2) throw new Error(`${label} must contain two numeric components.`);
467
- const value = [Number(fields[0]), Number(fields[1])] as const;
468
- if (value.some((component) => !Number.isFinite(component))) throw new Error(`${label} must contain finite numeric components.`);
469
- return value;
470
- }
471
-
472
- function parseColor3(fields: readonly string[], label: string): [number, number, number] {
473
- if (fields.length < 3) throw new Error(`${label} must contain three numeric components.`);
474
- const value: [number, number, number] = [
475
- clamp01(Number(fields[0])),
476
- clamp01(Number(fields[1])),
477
- clamp01(Number(fields[2]))
478
- ];
479
- if (value.some((component) => !Number.isFinite(component))) throw new Error(`${label} must contain finite numeric components.`);
480
- return value;
481
- }
482
-
483
- function parseFinite(value: string | undefined, label: string): number {
484
- const parsed = Number(value);
485
- if (!Number.isFinite(parsed)) throw new Error(`${label} must be finite.`);
486
- return parsed;
487
- }
488
-
489
- function clamp01(value: number): number {
490
- return Math.min(1, Math.max(0, value));
491
- }
492
-
493
- function faceNormal(a: Vec3, b: Vec3, c: Vec3): Vec3 {
494
- const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]] as const;
495
- const ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]] as const;
496
- return normalizeVec3([
497
- ab[1] * ac[2] - ab[2] * ac[1],
498
- ab[2] * ac[0] - ab[0] * ac[2],
499
- ab[0] * ac[1] - ab[1] * ac[0]
500
- ]);
501
- }
502
-
503
- function normalizeVec3(value: Vec3): Vec3 {
504
- const length = Math.hypot(value[0], value[1], value[2]);
505
- if (length <= 1e-8) return [0, 0, 1];
506
- return [value[0] / length, value[1] / length, value[2] / length];
507
- }
508
-
509
- function updateBounds(min: [number, number, number], max: [number, number, number], position: Vec3): void {
510
- min[0] = Math.min(min[0], position[0]);
511
- min[1] = Math.min(min[1], position[1]);
512
- min[2] = Math.min(min[2], position[2]);
513
- max[0] = Math.max(max[0], position[0]);
514
- max[1] = Math.max(max[1], position[1]);
515
- max[2] = Math.max(max[2], position[2]);
516
- }
517
-
518
- function objNameFromUrl(url: string): string {
519
- const path = url.split(/[?#]/, 1)[0] ?? url;
520
- const match = /([^/\\]+?)(?:\.obj)?$/i.exec(path);
521
- return sanitizeName(match?.[1] ?? "obj-asset");
522
- }
523
-
524
- function sanitizeName(value: string): string {
525
- const normalized = value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
526
- return normalized.length > 0 ? normalized : "obj-asset";
527
- }
528
-
529
- function decodeOBJDataUri(uri: string): Uint8Array {
530
- const separator = uri.indexOf(",");
531
- if (separator < 0) throw new Error("OBJ data uri must include a comma separator.");
532
- const header = uri.slice(5, separator).toLowerCase();
533
- const payload = uri.slice(separator + 1);
534
- if (header.endsWith(";base64")) return base64ToBytes(payload);
535
- return new TextEncoder().encode(decodeURIComponent(payload));
536
- }
537
-
538
- function bytesToBase64(bytes: Uint8Array): string {
539
- let binary = "";
540
- for (const byte of bytes) binary += String.fromCharCode(byte);
541
- return btoa(binary);
542
- }
543
-
544
- function base64ToBytes(payload: string): Uint8Array {
545
- const binary = atob(decodeURIComponent(payload).replace(/\s+/g, ""));
546
- const bytes = new Uint8Array(binary.length);
547
- for (let index = 0; index < binary.length; index += 1) {
548
- bytes[index] = binary.charCodeAt(index);
549
- }
550
- return bytes;
551
- }