@aura3d/assets 1.3.3 → 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.
- package/dist/GLTFLoader.js +1 -0
- package/dist/GLTFLoader.js.map +1 -1
- package/dist/GLTFRenderResources.d.ts.map +1 -1
- package/dist/GLTFRenderResources.js +1 -0
- package/dist/GLTFRenderResources.js.map +1 -1
- package/package.json +38 -5
- package/src/AdvancedAssetCorpus.ts +0 -454
- package/src/AssetBundleCacheFixtures.ts +0 -290
- package/src/AssetCache.ts +0 -76
- package/src/AssetCompatibility.ts +0 -224
- package/src/AssetCorpus.ts +0 -329
- package/src/AssetDependencyGraph.ts +0 -83
- package/src/AssetHandle.ts +0 -74
- package/src/AssetImportPreflight.ts +0 -179
- package/src/AssetInspection.ts +0 -706
- package/src/AssetLoader.ts +0 -36
- package/src/AssetManager.ts +0 -211
- package/src/AssetRegistry.ts +0 -44
- package/src/AssetRenderDefaults.ts +0 -36
- package/src/AudioLoader.ts +0 -41
- package/src/BlenderExportValidation.ts +0 -227
- package/src/CarConceptMaterialStability.ts +0 -1405
- package/src/EXRLoader.ts +0 -1
- package/src/ExternalParityGLTFCorpus.ts +0 -65
- package/src/GLTFAnimationRuntime.ts +0 -1416
- package/src/GLTFAutoFit.ts +0 -133
- package/src/GLTFCompressionDecoders.ts +0 -190
- package/src/GLTFExtensionSupport.ts +0 -138
- package/src/GLTFLoader.ts +0 -4059
- package/src/GLTFRenderResources.ts +0 -2269
- package/src/HDRLoader.ts +0 -1
- package/src/ImageLoader.ts +0 -52
- package/src/ImportPipeline.ts +0 -148
- package/src/KTX2BasisTextureTranscoder.ts +0 -225
- package/src/LoadContext.ts +0 -60
- package/src/MaterialLoader.ts +0 -166
- package/src/MeshOptimization.ts +0 -93
- package/src/OBJLoader.ts +0 -551
- package/src/SceneAnalysisFixtures.ts +0 -438
- package/src/SceneLoader.ts +0 -62
- package/src/ShaderLoader.ts +0 -30
- package/src/TextureLoader.ts +0 -30
- package/src/TexturePipeline.ts +0 -96
- package/src/TextureStreaming.ts +0 -20
- package/src/WorkerAssetJobs.ts +0 -56
- package/src/advanced-gallery/index.ts +0 -18
- package/src/asset-corpus/GLTFSceneLoader.ts +0 -9
- package/src/asset-corpus/ProductionAssetCorpus.ts +0 -230
- package/src/asset-corpus/ProductionAssetPipeline.ts +0 -5
- package/src/asset-corpus/ProductionGLTFRenderPipeline.ts +0 -182
- package/src/asset-corpus/ProductionHDRTextureLoader.ts +0 -2
- package/src/asset-corpus/ProductionKTX2TextureLoader.ts +0 -2
- package/src/asset-corpus/ProductionTextureLoader.ts +0 -2
- package/src/asset-corpus/index.ts +0 -29
- package/src/browser-index.ts +0 -229
- package/src/createRenderableScene.ts +0 -128
- package/src/index.ts +0 -325
- package/src/loadRenderableAsset.ts +0 -67
- package/src/loaders/CubeTextureLoader.ts +0 -8
- package/src/loaders/EXRLoader.ts +0 -10
- package/src/loaders/HDRLoader.ts +0 -7
- package/src/loaders/KTX2Loader.ts +0 -7
- package/src/loaders/LoaderDiagnostics.ts +0 -36
- package/src/loaders/MTLLoader.ts +0 -20
- package/src/loaders/OBJLoader.ts +0 -24
- package/src/loaders/TextureLoader.ts +0 -14
- package/src/loaders/ThreeCompatGLTFLoader.ts +0 -19
- package/src/loaders/index.ts +0 -14
- package/src/threejs-compatibility/ThreeCompatAssetProvenance.ts +0 -51
- package/src/threejs-compatibility/ThreeCompatAssetRegistry.ts +0 -61
- package/src/threejs-example-parity/index.ts +0 -262
- package/tests/assets.test.ts +0 -393
- package/tsconfig.json +0 -7
package/src/AssetLoader.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { LoadContext } from "./LoadContext";
|
|
2
|
-
|
|
3
|
-
export interface AssetLoadRequest {
|
|
4
|
-
readonly url: string;
|
|
5
|
-
readonly type?: string;
|
|
6
|
-
readonly signal?: AbortSignal;
|
|
7
|
-
readonly onProgress?: (event: AssetLoadProgress) => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface AssetLoadProgress {
|
|
11
|
-
readonly url: string;
|
|
12
|
-
readonly phase: "document" | "buffer" | "complete";
|
|
13
|
-
readonly loadedBytes: number;
|
|
14
|
-
readonly totalBytes?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface AssetLoader<T = unknown> {
|
|
18
|
-
readonly type: string;
|
|
19
|
-
canLoad(request: AssetLoadRequest): boolean;
|
|
20
|
-
dependencies?(request: AssetLoadRequest, context: LoadContext): Promise<readonly string[]> | readonly string[];
|
|
21
|
-
load(request: AssetLoadRequest, context: LoadContext): Promise<T> | T;
|
|
22
|
-
dispose?(asset: T): void | Promise<void>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class AssetLoadError extends Error {
|
|
26
|
-
readonly url: string;
|
|
27
|
-
readonly dependencyChain: readonly string[];
|
|
28
|
-
|
|
29
|
-
constructor(message: string, url: string, dependencyChain: readonly string[] = [], cause?: unknown) {
|
|
30
|
-
super(message);
|
|
31
|
-
this.name = "AssetLoadError";
|
|
32
|
-
this.url = url;
|
|
33
|
-
this.dependencyChain = dependencyChain;
|
|
34
|
-
this.cause = cause;
|
|
35
|
-
}
|
|
36
|
-
}
|
package/src/AssetManager.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { AssetCache } from "./AssetCache";
|
|
2
|
-
import { AssetDependencyGraph } from "./AssetDependencyGraph";
|
|
3
|
-
import { AssetHandle } from "./AssetHandle";
|
|
4
|
-
import { AssetLoadError, type AssetLoadProgress, type AssetLoadRequest, type AssetLoader } from "./AssetLoader";
|
|
5
|
-
import { AssetRegistry } from "./AssetRegistry";
|
|
6
|
-
import { LoadContext } from "./LoadContext";
|
|
7
|
-
|
|
8
|
-
export interface AssetManagerOptions {
|
|
9
|
-
readonly baseUrl?: string;
|
|
10
|
-
readonly retries?: number;
|
|
11
|
-
readonly retryDelayMs?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface AssetLoadOptions {
|
|
15
|
-
readonly type?: string;
|
|
16
|
-
readonly signal?: AbortSignal;
|
|
17
|
-
readonly dependencyChain?: readonly string[];
|
|
18
|
-
readonly retries?: number;
|
|
19
|
-
readonly retryDelayMs?: number;
|
|
20
|
-
readonly onProgress?: (event: AssetLoadProgress) => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class AssetManager {
|
|
24
|
-
readonly registry = new AssetRegistry();
|
|
25
|
-
readonly cache = new AssetCache();
|
|
26
|
-
readonly dependencies = new AssetDependencyGraph();
|
|
27
|
-
|
|
28
|
-
private readonly baseUrl: string;
|
|
29
|
-
private readonly defaultRetries: number;
|
|
30
|
-
private readonly defaultRetryDelayMs: number;
|
|
31
|
-
private nextId = 1;
|
|
32
|
-
|
|
33
|
-
constructor(options: AssetManagerOptions = {}) {
|
|
34
|
-
this.baseUrl = options.baseUrl ?? "";
|
|
35
|
-
this.defaultRetries = Math.max(0, Math.trunc(options.retries ?? 0));
|
|
36
|
-
this.defaultRetryDelayMs = Math.max(0, options.retryDelayMs ?? 0);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
register(loader: AssetLoader): void {
|
|
40
|
-
this.registry.register(loader);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async load<T = unknown>(url: string, options: AssetLoadOptions = {}): Promise<AssetHandle<T>> {
|
|
44
|
-
const context = new LoadContext({
|
|
45
|
-
baseUrl: this.baseUrl,
|
|
46
|
-
signal: options.signal,
|
|
47
|
-
dependencyChain: options.dependencyChain,
|
|
48
|
-
manager: this
|
|
49
|
-
});
|
|
50
|
-
const resolvedUrl = context.resolve(url);
|
|
51
|
-
context.throwIfAborted(resolvedUrl);
|
|
52
|
-
const key = `${options.type ?? "auto"}:${resolvedUrl}`;
|
|
53
|
-
|
|
54
|
-
const cached = this.cache.get<T>(key);
|
|
55
|
-
if (cached) {
|
|
56
|
-
return cached.retain();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const inFlight = this.cache.getInFlight<T>(key);
|
|
60
|
-
if (inFlight) {
|
|
61
|
-
const handle = await this.awaitInFlight(inFlight, context, resolvedUrl);
|
|
62
|
-
return handle.retain();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const request: AssetLoadRequest = { url: resolvedUrl, type: options.type, signal: options.signal, onProgress: options.onProgress };
|
|
66
|
-
const loader = this.registry.find(request);
|
|
67
|
-
|
|
68
|
-
const promise = this.loadWithLoader<T>(loader, request, context.child(resolvedUrl), {
|
|
69
|
-
retries: Math.max(0, Math.trunc(options.retries ?? this.defaultRetries)),
|
|
70
|
-
retryDelayMs: Math.max(0, options.retryDelayMs ?? this.defaultRetryDelayMs)
|
|
71
|
-
});
|
|
72
|
-
this.cache.setInFlight(key, promise);
|
|
73
|
-
return promise;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async release<T>(handle: AssetHandle<T>): Promise<void> {
|
|
77
|
-
const remaining = await handle.release();
|
|
78
|
-
if (remaining > 0) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
for (const dependencyId of this.dependencies.dependenciesOf(handle.id)) {
|
|
83
|
-
const dependencyKey = this.cache.keys().find((key) => this.cache.get(key)?.id === dependencyId);
|
|
84
|
-
const dependency = dependencyKey ? this.cache.get(dependencyKey) : undefined;
|
|
85
|
-
if (dependency && !dependency.disposed) {
|
|
86
|
-
await this.release(dependency);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const cacheKey = this.cache.keys().find((key) => this.cache.get(key)?.id === handle.id);
|
|
91
|
-
if (cacheKey) {
|
|
92
|
-
this.cache.delete(cacheKey);
|
|
93
|
-
}
|
|
94
|
-
this.dependencies.remove(handle.id);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private async loadWithLoader<T>(
|
|
98
|
-
loader: AssetLoader,
|
|
99
|
-
request: AssetLoadRequest,
|
|
100
|
-
context: LoadContext,
|
|
101
|
-
retry: { readonly retries: number; readonly retryDelayMs: number }
|
|
102
|
-
): Promise<AssetHandle<T>> {
|
|
103
|
-
let attempt = 0;
|
|
104
|
-
|
|
105
|
-
while (true) {
|
|
106
|
-
context.throwIfAborted(request.url);
|
|
107
|
-
const dependencyHandles: Array<AssetHandle<unknown>> = [];
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const dependencyUrls = await loader.dependencies?.(request, context);
|
|
111
|
-
|
|
112
|
-
for (const dependencyUrl of dependencyUrls ?? []) {
|
|
113
|
-
context.throwIfAborted(request.url);
|
|
114
|
-
dependencyHandles.push(await context.loadDependency(dependencyUrl));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
context.throwIfAborted(request.url);
|
|
118
|
-
const value = (await loader.load(request, context)) as T;
|
|
119
|
-
const handle = new AssetHandle<T>({
|
|
120
|
-
id: `asset-${this.nextId++}`,
|
|
121
|
-
url: request.url,
|
|
122
|
-
type: loader.type,
|
|
123
|
-
value,
|
|
124
|
-
dispose: (asset) => loader.dispose?.(asset)
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
this.dependencies.addNode(handle.id);
|
|
128
|
-
for (const dependency of dependencyHandles) {
|
|
129
|
-
this.dependencies.addDependency(handle.id, dependency.id);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return handle;
|
|
133
|
-
} catch (error) {
|
|
134
|
-
await this.releaseLoadedDependencies(dependencyHandles);
|
|
135
|
-
|
|
136
|
-
if (this.shouldRetry(error, context, attempt, retry.retries)) {
|
|
137
|
-
attempt += 1;
|
|
138
|
-
await this.waitForRetry(retry.retryDelayMs, context, request.url);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (error instanceof AssetLoadError) {
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
throw new AssetLoadError(`Failed to load asset ${request.url}`, request.url, context.dependencyChain, error);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private async releaseLoadedDependencies(dependencyHandles: readonly AssetHandle<unknown>[]): Promise<void> {
|
|
152
|
-
for (const dependency of [...dependencyHandles].reverse()) {
|
|
153
|
-
if (!dependency.disposed) {
|
|
154
|
-
await this.release(dependency);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private async awaitInFlight<T>(
|
|
160
|
-
promise: Promise<AssetHandle<T>>,
|
|
161
|
-
context: LoadContext,
|
|
162
|
-
url: string
|
|
163
|
-
): Promise<AssetHandle<T>> {
|
|
164
|
-
if (!context.signal) {
|
|
165
|
-
return promise;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
context.throwIfAborted(url);
|
|
169
|
-
|
|
170
|
-
return new Promise<AssetHandle<T>>((resolve, reject) => {
|
|
171
|
-
const abort = () => {
|
|
172
|
-
reject(new AssetLoadError("Asset load aborted", url, context.dependencyChain, context.signal?.reason));
|
|
173
|
-
};
|
|
174
|
-
context.signal?.addEventListener("abort", abort, { once: true });
|
|
175
|
-
promise.then(
|
|
176
|
-
(handle) => {
|
|
177
|
-
context.signal?.removeEventListener("abort", abort);
|
|
178
|
-
resolve(handle);
|
|
179
|
-
},
|
|
180
|
-
(error) => {
|
|
181
|
-
context.signal?.removeEventListener("abort", abort);
|
|
182
|
-
reject(error);
|
|
183
|
-
}
|
|
184
|
-
);
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private shouldRetry(error: unknown, context: LoadContext, attempt: number, retries: number): boolean {
|
|
189
|
-
if (attempt >= retries) return false;
|
|
190
|
-
if (context.signal?.aborted) return false;
|
|
191
|
-
if (error instanceof AssetLoadError && error.message === "Asset load aborted") return false;
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private async waitForRetry(delayMs: number, context: LoadContext, url: string): Promise<void> {
|
|
196
|
-
context.throwIfAborted(url);
|
|
197
|
-
if (delayMs === 0) return;
|
|
198
|
-
|
|
199
|
-
await new Promise<void>((resolve, reject) => {
|
|
200
|
-
const timeout = setTimeout(() => {
|
|
201
|
-
context.signal?.removeEventListener("abort", abort);
|
|
202
|
-
resolve();
|
|
203
|
-
}, delayMs);
|
|
204
|
-
const abort = () => {
|
|
205
|
-
clearTimeout(timeout);
|
|
206
|
-
reject(new AssetLoadError("Asset load aborted", url, context.dependencyChain, context.signal?.reason));
|
|
207
|
-
};
|
|
208
|
-
context.signal?.addEventListener("abort", abort, { once: true });
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
package/src/AssetRegistry.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { AssetLoadRequest, AssetLoader } from "./AssetLoader";
|
|
2
|
-
|
|
3
|
-
export class AssetRegistry {
|
|
4
|
-
private readonly loaders = new Map<string, AssetLoader>();
|
|
5
|
-
|
|
6
|
-
register(loader: AssetLoader): void {
|
|
7
|
-
if (this.loaders.has(loader.type)) {
|
|
8
|
-
throw new Error(`Asset loader type already registered: ${loader.type}`);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
this.loaders.set(loader.type, loader);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
unregister(type: string): boolean {
|
|
15
|
-
return this.loaders.delete(type);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
get(type: string): AssetLoader {
|
|
19
|
-
const loader = this.loaders.get(type);
|
|
20
|
-
if (!loader) {
|
|
21
|
-
throw new Error(`No asset loader registered for type: ${type}`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return loader;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
find(request: AssetLoadRequest): AssetLoader {
|
|
28
|
-
if (request.type) {
|
|
29
|
-
return this.get(request.type);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
for (const loader of this.loaders.values()) {
|
|
33
|
-
if (loader.canLoad(request)) {
|
|
34
|
-
return loader;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
throw new Error(`No asset loader can load ${request.url}`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
list(): readonly string[] {
|
|
42
|
-
return [...this.loaders.keys()];
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { CameraFrameViewport, PerspectiveCameraFrameOptions, RendererPostProcessOptions, RendererShadowOptions } from "@aura3d/rendering";
|
|
2
|
-
import { createLightingDefault } from "@aura3d/rendering";
|
|
3
|
-
|
|
4
|
-
export type AssetRenderLightingPreset = "studioProduct" | "outdoorDay" | "interiorGallery" | "gameNight";
|
|
5
|
-
|
|
6
|
-
export interface AssetRenderDefaults {
|
|
7
|
-
readonly viewport: CameraFrameViewport;
|
|
8
|
-
readonly camera: "auto-frame";
|
|
9
|
-
readonly frame: PerspectiveCameraFrameOptions;
|
|
10
|
-
readonly lighting: AssetRenderLightingPreset;
|
|
11
|
-
readonly shadows: RendererShadowOptions;
|
|
12
|
-
readonly postprocess: RendererPostProcessOptions;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const DEFAULT_ASSET_RENDER_VIEWPORT: CameraFrameViewport = {
|
|
16
|
-
width: 960,
|
|
17
|
-
height: 540
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function createAssetRenderDefaults(lighting: AssetRenderLightingPreset = "studioProduct"): AssetRenderDefaults {
|
|
21
|
-
const defaults = createLightingDefault(lighting);
|
|
22
|
-
return {
|
|
23
|
-
viewport: DEFAULT_ASSET_RENDER_VIEWPORT,
|
|
24
|
-
camera: "auto-frame",
|
|
25
|
-
frame: {
|
|
26
|
-
paddingRatio: 0.16,
|
|
27
|
-
yawRadians: -0.38,
|
|
28
|
-
pitchRadians: -0.16,
|
|
29
|
-
nearPadding: 0.18,
|
|
30
|
-
farPadding: 2.4
|
|
31
|
-
},
|
|
32
|
-
lighting,
|
|
33
|
-
shadows: defaults.shadow,
|
|
34
|
-
postprocess: defaults.postprocess
|
|
35
|
-
};
|
|
36
|
-
}
|
package/src/AudioLoader.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { AssetLoadRequest, AssetLoader } from "./AssetLoader";
|
|
2
|
-
import type { LoadContext } from "./LoadContext";
|
|
3
|
-
|
|
4
|
-
export interface AudioDecodeContext {
|
|
5
|
-
decodeAudioData(data: ArrayBuffer): Promise<AudioBuffer> | AudioBuffer;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface AudioAsset {
|
|
9
|
-
readonly url: string;
|
|
10
|
-
readonly buffer: AudioBuffer;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class AudioLoader implements AssetLoader<AudioAsset> {
|
|
14
|
-
readonly type = "audio";
|
|
15
|
-
|
|
16
|
-
private readonly audioContext: AudioDecodeContext;
|
|
17
|
-
|
|
18
|
-
constructor(audioContext: AudioDecodeContext) {
|
|
19
|
-
this.audioContext = audioContext;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
canLoad(request: AssetLoadRequest): boolean {
|
|
23
|
-
return /\.(?:mp3|wav|ogg|m4a|aac)(?:\?.*)?$/i.test(request.url);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async load(request: AssetLoadRequest, context: LoadContext): Promise<AudioAsset> {
|
|
27
|
-
context.throwIfAborted(request.url);
|
|
28
|
-
|
|
29
|
-
if (typeof fetch !== "function") {
|
|
30
|
-
throw new Error("AudioLoader requires fetch");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const response = await fetch(request.url, { signal: request.signal });
|
|
34
|
-
if (!response.ok) {
|
|
35
|
-
throw new Error(`Audio request failed with ${response.status}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const buffer = await this.audioContext.decodeAudioData(await response.arrayBuffer());
|
|
39
|
-
return { url: request.url, buffer };
|
|
40
|
-
}
|
|
41
|
-
}
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { GLTFLoader, type GLTFAsset } from "./GLTFLoader";
|
|
2
|
-
import { LoadContext } from "./LoadContext";
|
|
3
|
-
|
|
4
|
-
export type BlenderExportValidationStatus = "pass" | "warn" | "fail";
|
|
5
|
-
|
|
6
|
-
export interface BlenderExportFixtureManifest {
|
|
7
|
-
readonly schemaVersion: "blender-export-fixtures";
|
|
8
|
-
readonly sourceName: string;
|
|
9
|
-
readonly sourceRepository: string;
|
|
10
|
-
readonly sourceRevision: string;
|
|
11
|
-
readonly sourceReadme: string;
|
|
12
|
-
readonly sourceEvidence: string;
|
|
13
|
-
readonly fixtures: readonly BlenderExportFixture[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface BlenderExportFixture {
|
|
17
|
-
readonly id: string;
|
|
18
|
-
readonly name: string;
|
|
19
|
-
readonly path: string;
|
|
20
|
-
readonly upstreamPath: string;
|
|
21
|
-
readonly sourceSha256: string;
|
|
22
|
-
readonly license: string;
|
|
23
|
-
readonly expectedGeneratorPattern: string;
|
|
24
|
-
readonly tags: readonly string[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface BlenderExportFixtureInput extends BlenderExportFixture {
|
|
28
|
-
readonly sourceText: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface BlenderExportValidationReport {
|
|
32
|
-
readonly schemaVersion: "blender-export-validation";
|
|
33
|
-
readonly generatedAt: string;
|
|
34
|
-
readonly sourceManifest: Omit<BlenderExportFixtureManifest, "fixtures"> & { readonly fixtureCount: number };
|
|
35
|
-
readonly summary: Record<BlenderExportValidationStatus, number> & { readonly fixtureCount: number };
|
|
36
|
-
readonly fixtures: readonly BlenderExportValidationFixtureResult[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface BlenderExportValidationFixtureResult {
|
|
40
|
-
readonly id: string;
|
|
41
|
-
readonly name: string;
|
|
42
|
-
readonly path: string;
|
|
43
|
-
readonly upstreamPath: string;
|
|
44
|
-
readonly sourceSha256: string;
|
|
45
|
-
readonly license: string;
|
|
46
|
-
readonly tags: readonly string[];
|
|
47
|
-
readonly generator: string;
|
|
48
|
-
readonly status: BlenderExportValidationStatus;
|
|
49
|
-
readonly metrics: {
|
|
50
|
-
readonly scenes: number;
|
|
51
|
-
readonly meshes: number;
|
|
52
|
-
readonly materials: number;
|
|
53
|
-
readonly textures: number;
|
|
54
|
-
readonly images: number;
|
|
55
|
-
readonly animations: number;
|
|
56
|
-
readonly renderables: number;
|
|
57
|
-
};
|
|
58
|
-
readonly diagnostics: readonly BlenderExportValidationDiagnostic[];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface BlenderExportValidationDiagnostic {
|
|
62
|
-
readonly code: string;
|
|
63
|
-
readonly severity: "info" | "warning" | "error";
|
|
64
|
-
readonly message: string;
|
|
65
|
-
readonly nextAction: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function createBlenderExportValidationReport(
|
|
69
|
-
manifest: BlenderExportFixtureManifest,
|
|
70
|
-
fixtureInputs: readonly BlenderExportFixtureInput[],
|
|
71
|
-
generatedAt = new Date().toISOString()
|
|
72
|
-
): Promise<BlenderExportValidationReport> {
|
|
73
|
-
assertValidBlenderExportFixtureManifest(manifest);
|
|
74
|
-
const inputById = new Map(fixtureInputs.map((fixture) => [fixture.id, fixture]));
|
|
75
|
-
const fixtures = await Promise.all(manifest.fixtures.map(async (fixture) => validateFixture(fixture, inputById.get(fixture.id))));
|
|
76
|
-
return {
|
|
77
|
-
schemaVersion: "blender-export-validation",
|
|
78
|
-
generatedAt,
|
|
79
|
-
sourceManifest: {
|
|
80
|
-
schemaVersion: manifest.schemaVersion,
|
|
81
|
-
sourceName: manifest.sourceName,
|
|
82
|
-
sourceRepository: manifest.sourceRepository,
|
|
83
|
-
sourceRevision: manifest.sourceRevision,
|
|
84
|
-
sourceReadme: manifest.sourceReadme,
|
|
85
|
-
sourceEvidence: manifest.sourceEvidence,
|
|
86
|
-
fixtureCount: manifest.fixtures.length
|
|
87
|
-
},
|
|
88
|
-
summary: {
|
|
89
|
-
fixtureCount: fixtures.length,
|
|
90
|
-
pass: fixtures.filter((fixture) => fixture.status === "pass").length,
|
|
91
|
-
warn: fixtures.filter((fixture) => fixture.status === "warn").length,
|
|
92
|
-
fail: fixtures.filter((fixture) => fixture.status === "fail").length
|
|
93
|
-
},
|
|
94
|
-
fixtures
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function assertValidBlenderExportFixtureManifest(manifest: BlenderExportFixtureManifest): BlenderExportFixtureManifest {
|
|
99
|
-
if (manifest.schemaVersion !== "blender-export-fixtures") {
|
|
100
|
-
throw new Error("Blender export fixture manifest schemaVersion must be blender-export-fixtures");
|
|
101
|
-
}
|
|
102
|
-
if (!manifest.sourceRepository || !/^https:\/\/github\.com\/KhronosGroup\/Vulkan-Samples-Assets/.test(manifest.sourceRepository)) {
|
|
103
|
-
throw new Error("Blender export fixture manifest must reference KhronosGroup/Vulkan-Samples-Assets");
|
|
104
|
-
}
|
|
105
|
-
if (!/^[0-9a-f]{40}$/i.test(manifest.sourceRevision)) {
|
|
106
|
-
throw new Error("Blender export fixture manifest sourceRevision must be a git SHA");
|
|
107
|
-
}
|
|
108
|
-
if (manifest.fixtures.length < 2) {
|
|
109
|
-
throw new Error("Blender export validation requires multiple fixtures");
|
|
110
|
-
}
|
|
111
|
-
const ids = new Set<string>();
|
|
112
|
-
for (const fixture of manifest.fixtures) {
|
|
113
|
-
if (ids.has(fixture.id)) {
|
|
114
|
-
throw new Error(`Blender export fixture id is duplicated: ${fixture.id}`);
|
|
115
|
-
}
|
|
116
|
-
ids.add(fixture.id);
|
|
117
|
-
if (!fixture.path.endsWith(".gltf") && !fixture.path.endsWith(".glb")) {
|
|
118
|
-
throw new Error(`Blender export fixture ${fixture.id} must be a glTF or GLB file`);
|
|
119
|
-
}
|
|
120
|
-
if (!/^[0-9a-f]{64}$/i.test(fixture.sourceSha256)) {
|
|
121
|
-
throw new Error(`Blender export fixture ${fixture.id} sourceSha256 must be a SHA-256 digest`);
|
|
122
|
-
}
|
|
123
|
-
if (!fixture.expectedGeneratorPattern) {
|
|
124
|
-
throw new Error(`Blender export fixture ${fixture.id} must define expectedGeneratorPattern`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return manifest;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function validateFixture(
|
|
131
|
-
fixture: BlenderExportFixture,
|
|
132
|
-
input: BlenderExportFixtureInput | undefined
|
|
133
|
-
): Promise<BlenderExportValidationFixtureResult> {
|
|
134
|
-
if (!input) {
|
|
135
|
-
return failureFixture(fixture, "ASSET_BLENDER_EXPORT_SOURCE_MISSING", "Fixture source text was not provided to the validation runner.");
|
|
136
|
-
}
|
|
137
|
-
try {
|
|
138
|
-
const json = JSON.parse(input.sourceText) as { readonly asset?: { readonly generator?: string } };
|
|
139
|
-
const generator = json.asset?.generator ?? "";
|
|
140
|
-
const diagnostics: BlenderExportValidationDiagnostic[] = [];
|
|
141
|
-
if (!new RegExp(fixture.expectedGeneratorPattern, "i").test(generator)) {
|
|
142
|
-
diagnostics.push({
|
|
143
|
-
code: "ASSET_BLENDER_EXPORT_GENERATOR_MISMATCH",
|
|
144
|
-
severity: "error",
|
|
145
|
-
message: `Expected generator matching ${fixture.expectedGeneratorPattern}, got ${generator || "missing"}.`,
|
|
146
|
-
nextAction: "Replace the fixture with a confirmed Blender-exported glTF file or update the manifest evidence."
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
const asset = await new GLTFLoader().load({ url: dataGLTF(input.sourceText) }, new LoadContext());
|
|
150
|
-
return resultForAsset(fixture, asset, generator, diagnostics);
|
|
151
|
-
} catch (error) {
|
|
152
|
-
return failureFixture(fixture, "ASSET_BLENDER_EXPORT_LOAD_FAILED", error instanceof Error ? error.message : String(error));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function resultForAsset(
|
|
157
|
-
fixture: BlenderExportFixture,
|
|
158
|
-
asset: GLTFAsset,
|
|
159
|
-
generator: string,
|
|
160
|
-
diagnostics: readonly BlenderExportValidationDiagnostic[]
|
|
161
|
-
): BlenderExportValidationFixtureResult {
|
|
162
|
-
const renderables = asset.createScene().collectRenderables().length;
|
|
163
|
-
const status = diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "fail" : diagnostics.length > 0 ? "warn" : "pass";
|
|
164
|
-
return {
|
|
165
|
-
id: fixture.id,
|
|
166
|
-
name: fixture.name,
|
|
167
|
-
path: fixture.path,
|
|
168
|
-
upstreamPath: fixture.upstreamPath,
|
|
169
|
-
sourceSha256: fixture.sourceSha256,
|
|
170
|
-
license: fixture.license,
|
|
171
|
-
tags: fixture.tags,
|
|
172
|
-
generator,
|
|
173
|
-
status,
|
|
174
|
-
metrics: {
|
|
175
|
-
scenes: asset.scenes.length,
|
|
176
|
-
meshes: asset.meshes.length,
|
|
177
|
-
materials: asset.materials.length,
|
|
178
|
-
textures: asset.textures.length,
|
|
179
|
-
images: asset.images.length,
|
|
180
|
-
animations: asset.animations.length,
|
|
181
|
-
renderables
|
|
182
|
-
},
|
|
183
|
-
diagnostics: diagnostics.length > 0 ? diagnostics : [{
|
|
184
|
-
code: "ASSET_BLENDER_EXPORT_VALIDATED",
|
|
185
|
-
severity: "info",
|
|
186
|
-
message: "Fixture was confirmed as Blender-exported glTF and loaded through Aura3D's glTF loader.",
|
|
187
|
-
nextAction: "Keep this fixture current when changing loader validation, importer settings, or fixture source revision."
|
|
188
|
-
}]
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function failureFixture(
|
|
193
|
-
fixture: BlenderExportFixture,
|
|
194
|
-
code: string,
|
|
195
|
-
message: string
|
|
196
|
-
): BlenderExportValidationFixtureResult {
|
|
197
|
-
return {
|
|
198
|
-
id: fixture.id,
|
|
199
|
-
name: fixture.name,
|
|
200
|
-
path: fixture.path,
|
|
201
|
-
upstreamPath: fixture.upstreamPath,
|
|
202
|
-
sourceSha256: fixture.sourceSha256,
|
|
203
|
-
license: fixture.license,
|
|
204
|
-
tags: fixture.tags,
|
|
205
|
-
generator: "",
|
|
206
|
-
status: "fail",
|
|
207
|
-
metrics: {
|
|
208
|
-
scenes: 0,
|
|
209
|
-
meshes: 0,
|
|
210
|
-
materials: 0,
|
|
211
|
-
textures: 0,
|
|
212
|
-
images: 0,
|
|
213
|
-
animations: 0,
|
|
214
|
-
renderables: 0
|
|
215
|
-
},
|
|
216
|
-
diagnostics: [{
|
|
217
|
-
code,
|
|
218
|
-
severity: "error",
|
|
219
|
-
message,
|
|
220
|
-
nextAction: "Fix or replace this Blender-export fixture before marking Blender-export validation complete."
|
|
221
|
-
}]
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function dataGLTF(source: string): string {
|
|
226
|
-
return `data:model/gltf+json,${encodeURIComponent(source)}`;
|
|
227
|
-
}
|