@aura3d/cli 1.0.5
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/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +265 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +355 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1304 -0
- package/dist/index.js.map +1 -0
- package/dist/pull-bridge.d.ts +95 -0
- package/dist/pull-bridge.d.ts.map +1 -0
- package/dist/pull-bridge.js +247 -0
- package/dist/pull-bridge.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1304 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
4
|
+
export const DEFAULT_AURA_ASSET_MANIFEST = "aura.assets.json";
|
|
5
|
+
export const DEFAULT_AURA_ASSET_OUTPUT_DIR = "public/aura-assets";
|
|
6
|
+
export const DEFAULT_AURA_ASSET_PUBLIC_PATH = "/aura-assets/";
|
|
7
|
+
export const DEFAULT_AURA_ASSET_TYPEGEN = "src/aura-assets.ts";
|
|
8
|
+
export function addAsset(options) {
|
|
9
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
10
|
+
const sourcePath = resolve(projectDir, options.file);
|
|
11
|
+
if (!existsSync(sourcePath)) {
|
|
12
|
+
throw new Error(`Aura3D assets add failed: "${options.file}" does not exist. Suggested fix: pass a real local GLB/glTF/texture path.`);
|
|
13
|
+
}
|
|
14
|
+
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
15
|
+
const current = readAssetManifest(projectDir);
|
|
16
|
+
const outputDir = normalizeRelativePath(options.outputDir ?? current.outputDir ?? DEFAULT_AURA_ASSET_OUTPUT_DIR);
|
|
17
|
+
const publicPath = normalizePublicPath(options.publicPath ?? current.assetBasePath ?? DEFAULT_AURA_ASSET_PUBLIC_PATH);
|
|
18
|
+
const typegen = normalizeRelativePath(options.typegen ?? current.typegen ?? DEFAULT_AURA_ASSET_TYPEGEN);
|
|
19
|
+
const hash = hashFile(sourcePath);
|
|
20
|
+
const format = extname(sourcePath).slice(1).toLowerCase();
|
|
21
|
+
const type = options.type ?? inferAssetType(format);
|
|
22
|
+
const inspection = inspectAssetFile(sourcePath, format);
|
|
23
|
+
const outputFileName = `${options.name}.${hash.slice(0, 8)}.${format}`;
|
|
24
|
+
const outputPath = join(outputDir, outputFileName);
|
|
25
|
+
if (options.copy !== false) {
|
|
26
|
+
mkdirSync(resolve(projectDir, outputDir), { recursive: true });
|
|
27
|
+
copyFileSync(sourcePath, resolve(projectDir, outputPath));
|
|
28
|
+
copyAssetDependencies(projectDir, sourcePath, outputDir, inspection.dependencies);
|
|
29
|
+
}
|
|
30
|
+
const thumbnailUrl = writeThumbnail(projectDir, outputDir, publicPath, options.name, inspection.bounds);
|
|
31
|
+
const entry = {
|
|
32
|
+
id: options.name,
|
|
33
|
+
type,
|
|
34
|
+
format,
|
|
35
|
+
source: normalizeRelativePath(relative(projectDir, sourcePath)),
|
|
36
|
+
outputPath: normalizeRelativePath(outputPath),
|
|
37
|
+
url: `${publicPath}${outputFileName}`,
|
|
38
|
+
hash: `sha256-${hash}`,
|
|
39
|
+
sizeBytes: statSync(sourcePath).size,
|
|
40
|
+
bounds: inspection.bounds,
|
|
41
|
+
boundsMetadata: inspection.boundsMetadata,
|
|
42
|
+
materials: inspection.materials,
|
|
43
|
+
materialMetadata: inspection.materialMetadata,
|
|
44
|
+
animations: inspection.animations,
|
|
45
|
+
animationMetadata: inspection.animation,
|
|
46
|
+
humanoid: inspection.humanoid,
|
|
47
|
+
skeleton: inspection.skeleton,
|
|
48
|
+
morphTargets: inspection.morphTargets,
|
|
49
|
+
provenance: createAssetProvenance(projectDir, sourcePath, options, inspection.provenance),
|
|
50
|
+
textures: inspection.textures,
|
|
51
|
+
dependencies: inspection.dependencies,
|
|
52
|
+
orientation: inspection.orientation,
|
|
53
|
+
nodeNames: inspection.nodeNames,
|
|
54
|
+
thumbnailUrl,
|
|
55
|
+
warnings: createAssetWarnings(sourcePath, inspection)
|
|
56
|
+
};
|
|
57
|
+
const manifest = sortManifest({
|
|
58
|
+
schema: "aura3d.assets/1.0",
|
|
59
|
+
assetBasePath: publicPath,
|
|
60
|
+
outputDir,
|
|
61
|
+
typegen,
|
|
62
|
+
assets: [
|
|
63
|
+
...current.assets.filter((asset) => asset.id !== entry.id),
|
|
64
|
+
entry
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
writeAssetManifest(projectDir, manifest);
|
|
68
|
+
writeTypedAssets(projectDir, manifest);
|
|
69
|
+
return {
|
|
70
|
+
ok: true,
|
|
71
|
+
manifestPath,
|
|
72
|
+
manifest,
|
|
73
|
+
messages: [
|
|
74
|
+
`Added ${entry.id} -> ${entry.url}`,
|
|
75
|
+
`Wrote ${DEFAULT_AURA_ASSET_MANIFEST}`,
|
|
76
|
+
`Wrote ${manifest.typegen}`
|
|
77
|
+
]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function scanAssets(options) {
|
|
81
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
82
|
+
const directory = resolve(projectDir, options.directory);
|
|
83
|
+
if (!existsSync(directory))
|
|
84
|
+
throw new Error(`Aura3D assets scan failed: "${options.directory}" does not exist.`);
|
|
85
|
+
let result;
|
|
86
|
+
for (const file of readdirSync(directory)) {
|
|
87
|
+
const path = join(directory, file);
|
|
88
|
+
if (!statSync(path).isFile())
|
|
89
|
+
continue;
|
|
90
|
+
const format = extname(path).slice(1).toLowerCase();
|
|
91
|
+
if (!["glb", "gltf", "png", "jpg", "jpeg", "webp", "ktx2", "hdr", "exr", "mp3", "wav", "ogg"].includes(format))
|
|
92
|
+
continue;
|
|
93
|
+
const name = sanitizeAssetId(file.replace(/\.[^.]+$/, ""));
|
|
94
|
+
result = addAsset({ projectDir, file: relative(projectDir, path), name });
|
|
95
|
+
}
|
|
96
|
+
return result ?? {
|
|
97
|
+
ok: true,
|
|
98
|
+
manifestPath: resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
|
|
99
|
+
manifest: readAssetManifest(projectDir),
|
|
100
|
+
messages: ["No supported assets found."]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function inspectAsset(options) {
|
|
104
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
105
|
+
const sourcePath = resolve(projectDir, options.file);
|
|
106
|
+
if (!existsSync(sourcePath)) {
|
|
107
|
+
throw new Error(`Aura3D assets inspect failed: "${options.file}" does not exist.`);
|
|
108
|
+
}
|
|
109
|
+
const format = extname(sourcePath).slice(1).toLowerCase();
|
|
110
|
+
const inspection = inspectAssetFile(sourcePath, format);
|
|
111
|
+
const warnings = createAssetWarnings(sourcePath, inspection);
|
|
112
|
+
return {
|
|
113
|
+
ok: warnings.length === 0,
|
|
114
|
+
schema: "aura3d.asset-inspection/1.0",
|
|
115
|
+
file: normalizeRelativePath(relative(projectDir, sourcePath)),
|
|
116
|
+
format,
|
|
117
|
+
sizeBytes: statSync(sourcePath).size,
|
|
118
|
+
bounds: inspection.bounds,
|
|
119
|
+
boundsMetadata: inspection.boundsMetadata,
|
|
120
|
+
materials: inspection.materials,
|
|
121
|
+
materialMetadata: inspection.materialMetadata,
|
|
122
|
+
animations: inspection.animations,
|
|
123
|
+
...(options.animation ? { animation: inspection.animation } : {}),
|
|
124
|
+
...(options.humanoid ? { humanoid: inspection.humanoid } : {}),
|
|
125
|
+
...(options.skeleton ? { skeleton: inspection.skeleton } : {}),
|
|
126
|
+
...(options.morphs ? { morphTargets: inspection.morphTargets } : {}),
|
|
127
|
+
...(options.license ? { provenance: createAssetProvenance(projectDir, sourcePath, {}, inspection.provenance) } : {}),
|
|
128
|
+
textures: inspection.textures,
|
|
129
|
+
orientation: inspection.orientation,
|
|
130
|
+
nodeNames: inspection.nodeNames,
|
|
131
|
+
dependencies: inspection.dependencies,
|
|
132
|
+
warnings,
|
|
133
|
+
messages: warnings.length === 0 ? ["Asset inspection completed."] : warnings
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function validateAssets(options = {}) {
|
|
137
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
138
|
+
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
139
|
+
const manifestMissing = !existsSync(manifestPath);
|
|
140
|
+
const sourceManifest = readAssetManifest(projectDir);
|
|
141
|
+
const manifest = filterAssetManifest(sourceManifest, options.assetIds);
|
|
142
|
+
const externalProvenance = readExternalProvenance(projectDir, options.provenanceFile);
|
|
143
|
+
const failures = manifestMissing
|
|
144
|
+
? [`Missing ${DEFAULT_AURA_ASSET_MANIFEST}. Suggested fix: run aura3d assets add ./asset.glb --name product or aura3d assets scan ./assets.`]
|
|
145
|
+
: [];
|
|
146
|
+
const missingAssetIds = findMissingAssetIds(sourceManifest, options.assetIds);
|
|
147
|
+
for (const id of missingAssetIds)
|
|
148
|
+
failures.push(`Requested asset "${id}" was not found in ${DEFAULT_AURA_ASSET_MANIFEST}.`);
|
|
149
|
+
if (options.provenanceFile && !existsSync(resolve(projectDir, options.provenanceFile))) {
|
|
150
|
+
failures.push(`Missing asset provenance evidence file: ${options.provenanceFile}`);
|
|
151
|
+
}
|
|
152
|
+
const warnings = [];
|
|
153
|
+
for (const asset of manifest.assets) {
|
|
154
|
+
const outputPath = resolve(projectDir, asset.outputPath);
|
|
155
|
+
if (!existsSync(outputPath)) {
|
|
156
|
+
failures.push(`Missing asset output for "${asset.id}": ${asset.outputPath}`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const actualHash = `sha256-${hashFile(outputPath)}`;
|
|
160
|
+
if (actualHash !== asset.hash)
|
|
161
|
+
failures.push(`Hash mismatch for "${asset.id}": expected ${asset.hash}, found ${actualHash}`);
|
|
162
|
+
const provenance = resolveAssetProvenance(asset, externalProvenance);
|
|
163
|
+
if (options.noPlaceholders && isPlaceholderAsset(asset, provenance)) {
|
|
164
|
+
failures.push(`Placeholder asset is not allowed in strict release validation: "${asset.id}". Replace it with a real typed asset and provenance.`);
|
|
165
|
+
}
|
|
166
|
+
if (options.requireLicense && !hasUsableLicenseEvidence(provenance)) {
|
|
167
|
+
failures.push(`Missing license/provenance evidence for "${asset.id}". Add it with assets add --license ... --source-url ... or pass --provenance <evidence.json>.`);
|
|
168
|
+
}
|
|
169
|
+
warnings.push(...asset.warnings.map((warning) => `${asset.id}: ${warning}`));
|
|
170
|
+
if (asset.format === "gltf") {
|
|
171
|
+
for (const dependency of asset.dependencies ?? asset.textures) {
|
|
172
|
+
if (dependency.startsWith("data:"))
|
|
173
|
+
continue;
|
|
174
|
+
const sourceDependencyPath = resolve(dirname(resolve(projectDir, asset.source)), dependency);
|
|
175
|
+
const outputDependencyPath = resolve(dirname(resolve(projectDir, asset.outputPath)), dependency);
|
|
176
|
+
if (!existsSync(sourceDependencyPath) && !existsSync(outputDependencyPath)) {
|
|
177
|
+
failures.push(`Missing referenced asset dependency for "${asset.id}": ${dependency}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const typegenPath = resolve(projectDir, manifest.typegen);
|
|
183
|
+
if (!existsSync(typegenPath))
|
|
184
|
+
failures.push(`Missing typed asset module: ${manifest.typegen}. Run assets typegen.`);
|
|
185
|
+
return {
|
|
186
|
+
ok: failures.length === 0,
|
|
187
|
+
manifestPath,
|
|
188
|
+
manifest,
|
|
189
|
+
failures,
|
|
190
|
+
warnings,
|
|
191
|
+
messages: failures.length === 0 ? ["Asset manifest is valid."] : failures
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function writeTypedAssets(projectDir, manifest = readAssetManifest(projectDir)) {
|
|
195
|
+
const path = resolve(projectDir, manifest.typegen);
|
|
196
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
197
|
+
const lines = [
|
|
198
|
+
`import { defineAuraAssets } from "@aura3d/engine";`,
|
|
199
|
+
"",
|
|
200
|
+
"export const assets = defineAuraAssets({",
|
|
201
|
+
...manifest.assets.map((asset) => {
|
|
202
|
+
const metadata = {
|
|
203
|
+
materials: asset.materials,
|
|
204
|
+
animations: asset.animations,
|
|
205
|
+
animationClips: asset.animations,
|
|
206
|
+
animationMetadata: asset.animationMetadata ?? createReadinessAnimationMetadata(asset.animations),
|
|
207
|
+
humanoid: asset.humanoid?.humanoid ?? false,
|
|
208
|
+
humanoidStatus: asset.humanoid?.status ?? "unknown",
|
|
209
|
+
humanoidConfidence: asset.humanoid?.confidence ?? "low",
|
|
210
|
+
skeleton: asset.skeleton,
|
|
211
|
+
morphTargets: asset.morphTargets,
|
|
212
|
+
provenance: asset.provenance,
|
|
213
|
+
sourcePath: asset.source,
|
|
214
|
+
outputPath: asset.outputPath,
|
|
215
|
+
license: asset.provenance?.license,
|
|
216
|
+
author: asset.provenance?.author,
|
|
217
|
+
boundsMetadata: asset.boundsMetadata,
|
|
218
|
+
materialMetadata: asset.materialMetadata,
|
|
219
|
+
orientation: asset.orientation,
|
|
220
|
+
nodeNames: asset.nodeNames ?? [],
|
|
221
|
+
textures: asset.textures,
|
|
222
|
+
dependencies: asset.dependencies ?? [],
|
|
223
|
+
thumbnailUrl: asset.thumbnailUrl
|
|
224
|
+
};
|
|
225
|
+
const fields = [
|
|
226
|
+
`type: ${JSON.stringify(asset.type)}`,
|
|
227
|
+
`format: ${JSON.stringify(asset.format)}`,
|
|
228
|
+
`url: ${JSON.stringify(asset.url)}`,
|
|
229
|
+
`hash: ${JSON.stringify(asset.hash)}`,
|
|
230
|
+
`bounds: ${JSON.stringify(asset.bounds ?? [0, 0, 0])}`,
|
|
231
|
+
`sizeBytes: ${asset.sizeBytes}`,
|
|
232
|
+
`metadata: ${JSON.stringify(metadata)}`
|
|
233
|
+
];
|
|
234
|
+
return ` ${JSON.stringify(asset.id)}: { ${fields.join(", ")} },`;
|
|
235
|
+
}),
|
|
236
|
+
"} as const);",
|
|
237
|
+
"",
|
|
238
|
+
"export type AuraGeneratedAssets = typeof assets;",
|
|
239
|
+
""
|
|
240
|
+
];
|
|
241
|
+
writeFileSync(path, lines.join("\n"));
|
|
242
|
+
return path;
|
|
243
|
+
}
|
|
244
|
+
export function listAssets(options = {}) {
|
|
245
|
+
return readAssetManifest(resolve(options.projectDir ?? process.cwd())).assets;
|
|
246
|
+
}
|
|
247
|
+
export function createAssetThumbnails(options = {}) {
|
|
248
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
249
|
+
const manifest = readAssetManifest(projectDir);
|
|
250
|
+
const assets = manifest.assets.map((asset) => ({
|
|
251
|
+
...asset,
|
|
252
|
+
thumbnailUrl: writeThumbnail(projectDir, manifest.outputDir, manifest.assetBasePath, asset.id, asset.bounds)
|
|
253
|
+
}));
|
|
254
|
+
const next = sortManifest({ ...manifest, assets });
|
|
255
|
+
writeAssetManifest(projectDir, next);
|
|
256
|
+
writeTypedAssets(projectDir, next);
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
manifestPath: resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
|
|
260
|
+
manifest: next,
|
|
261
|
+
messages: [`Generated ${next.assets.length} thumbnails.`]
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export function doctor(options = {}) {
|
|
265
|
+
const validation = validateAssets(options);
|
|
266
|
+
const packagePath = resolve(options.projectDir ?? process.cwd(), "package.json");
|
|
267
|
+
const failures = [...validation.failures];
|
|
268
|
+
if (!existsSync(packagePath))
|
|
269
|
+
failures.push("Missing package.json.");
|
|
270
|
+
return {
|
|
271
|
+
...validation,
|
|
272
|
+
ok: failures.length === 0,
|
|
273
|
+
failures,
|
|
274
|
+
messages: failures.length === 0 ? ["Aura3D project doctor passed."] : failures
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
export function checkDeploy(options = {}) {
|
|
278
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
279
|
+
const manifest = readAssetManifest(projectDir);
|
|
280
|
+
const distDir = normalizeRelativePath(options.distDir ?? "dist");
|
|
281
|
+
const failures = [];
|
|
282
|
+
const warnings = [];
|
|
283
|
+
for (const asset of manifest.assets) {
|
|
284
|
+
const distPath = resolve(projectDir, distDir, asset.url.replace(/^\//, ""));
|
|
285
|
+
const publicPath = resolve(projectDir, asset.outputPath);
|
|
286
|
+
if (!existsSync(distPath) && !existsSync(publicPath)) {
|
|
287
|
+
failures.push(`Deploy check missing hashed asset for "${asset.id}": expected ${asset.url} in ${distDir} or ${asset.outputPath}`);
|
|
288
|
+
}
|
|
289
|
+
if (!/[a-f0-9]{8}\.[^.]+$/i.test(asset.url))
|
|
290
|
+
warnings.push(`${asset.id}: URL is not fingerprinted: ${asset.url}`);
|
|
291
|
+
}
|
|
292
|
+
const validation = validateAssets({ projectDir });
|
|
293
|
+
failures.push(...validation.failures);
|
|
294
|
+
warnings.push(...validation.warnings);
|
|
295
|
+
return {
|
|
296
|
+
ok: failures.length === 0,
|
|
297
|
+
manifestPath: resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
|
|
298
|
+
manifest,
|
|
299
|
+
failures,
|
|
300
|
+
warnings,
|
|
301
|
+
messages: failures.length === 0 ? ["Deploy check passed."] : failures
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
export function validateGameAssets(options = {}) {
|
|
305
|
+
return validateAssetReadiness("game", options);
|
|
306
|
+
}
|
|
307
|
+
export function validateCartoonAssets(options = {}) {
|
|
308
|
+
return validateAssetReadiness("cartoon", options);
|
|
309
|
+
}
|
|
310
|
+
export function createCharacterAssemblyPlan(options) {
|
|
311
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
312
|
+
const manifest = readAssetManifest(projectDir);
|
|
313
|
+
const failures = [];
|
|
314
|
+
const warnings = [];
|
|
315
|
+
const output = normalizeRelativePath(options.output ?? `src/aura-character-${sanitizeAssetId(options.name)}.assembly.json`);
|
|
316
|
+
const bodyAsset = manifest.assets.find((asset) => asset.id === options.body);
|
|
317
|
+
if (!bodyAsset) {
|
|
318
|
+
failures.push(`Missing body asset "${options.body}". Add it first with aura3d assets add ./body.glb --name ${options.body}.`);
|
|
319
|
+
}
|
|
320
|
+
else if (bodyAsset.type !== "model") {
|
|
321
|
+
failures.push(`Body asset "${options.body}" must be a model asset, found ${bodyAsset.type}.`);
|
|
322
|
+
}
|
|
323
|
+
else if (bodyAsset.humanoid && !bodyAsset.humanoid.humanoid) {
|
|
324
|
+
warnings.push(`Body asset "${options.body}" has humanoid status "${bodyAsset.humanoid.status}"; character assembly can still compose parts, but acting and retargeting may be limited.`);
|
|
325
|
+
}
|
|
326
|
+
const resolvePart = (part) => {
|
|
327
|
+
const asset = manifest.assets.find((entry) => entry.id === part.asset);
|
|
328
|
+
if (!asset) {
|
|
329
|
+
failures.push(`Missing ${part.slot} part asset "${part.asset}".`);
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
if (asset.type !== "model")
|
|
333
|
+
warnings.push(`${part.slot}: "${part.asset}" is ${asset.type}; character assembly expects model parts for rig/attachment safety.`);
|
|
334
|
+
return {
|
|
335
|
+
slot: part.slot,
|
|
336
|
+
asset: part.asset,
|
|
337
|
+
url: asset.url,
|
|
338
|
+
type: asset.type,
|
|
339
|
+
format: asset.format,
|
|
340
|
+
animations: asset.animations,
|
|
341
|
+
humanoid: asset.humanoid,
|
|
342
|
+
attachTo: part.attachTo ?? defaultAttachPoint(part.slot)
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
const parts = (options.parts ?? []).map(resolvePart).filter((part) => Boolean(part));
|
|
346
|
+
const body = bodyAsset
|
|
347
|
+
? {
|
|
348
|
+
slot: "body",
|
|
349
|
+
asset: bodyAsset.id,
|
|
350
|
+
url: bodyAsset.url,
|
|
351
|
+
type: bodyAsset.type,
|
|
352
|
+
format: bodyAsset.format,
|
|
353
|
+
animations: bodyAsset.animations,
|
|
354
|
+
humanoid: bodyAsset.humanoid,
|
|
355
|
+
attachTo: "root"
|
|
356
|
+
}
|
|
357
|
+
: {
|
|
358
|
+
slot: "body",
|
|
359
|
+
asset: options.body,
|
|
360
|
+
url: "",
|
|
361
|
+
type: "model",
|
|
362
|
+
format: "missing",
|
|
363
|
+
animations: [],
|
|
364
|
+
attachTo: "root"
|
|
365
|
+
};
|
|
366
|
+
const plan = {
|
|
367
|
+
schema: "aura3d.character-assembly/1.0",
|
|
368
|
+
name: options.name,
|
|
369
|
+
output,
|
|
370
|
+
scale: options.scale ?? 1,
|
|
371
|
+
body,
|
|
372
|
+
parts,
|
|
373
|
+
rules: {
|
|
374
|
+
normalizeScale: true,
|
|
375
|
+
facePositiveZ: true,
|
|
376
|
+
preserveTypedAssetReferences: true,
|
|
377
|
+
requireNamedAttachments: true
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
mkdirSync(dirname(resolve(projectDir, output)), { recursive: true });
|
|
381
|
+
writeFileSync(resolve(projectDir, output), `${JSON.stringify(plan, null, 2)}\n`);
|
|
382
|
+
return {
|
|
383
|
+
ok: failures.length === 0,
|
|
384
|
+
schema: "aura3d.character-assembly/1.0",
|
|
385
|
+
name: options.name,
|
|
386
|
+
output,
|
|
387
|
+
body,
|
|
388
|
+
parts,
|
|
389
|
+
validation: { failures, warnings },
|
|
390
|
+
messages: failures.length === 0
|
|
391
|
+
? [`Wrote ${output}. Import typed assets from src/aura-assets.ts and compose with model(assets.${options.body}).`]
|
|
392
|
+
: failures
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
export function initAgentFiles(options) {
|
|
396
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
397
|
+
const targets = options.agent === "all" ? ["generic", "claude", "cursor", "copilot"] : [options.agent];
|
|
398
|
+
const written = [];
|
|
399
|
+
for (const target of targets) {
|
|
400
|
+
if (target === "generic")
|
|
401
|
+
written.push(writeAgentFile(projectDir, "AGENTS.md", genericAgentText()));
|
|
402
|
+
if (target === "claude")
|
|
403
|
+
written.push(writeAgentFile(projectDir, ".claude/CLAUDE.md", genericAgentText("Claude")));
|
|
404
|
+
if (target === "cursor")
|
|
405
|
+
written.push(writeAgentFile(projectDir, ".cursor/rules/aura3d.mdc", genericAgentText("Cursor")));
|
|
406
|
+
if (target === "copilot")
|
|
407
|
+
written.push(writeAgentFile(projectDir, ".github/copilot-instructions.md", genericAgentText("GitHub Copilot")));
|
|
408
|
+
}
|
|
409
|
+
return written;
|
|
410
|
+
}
|
|
411
|
+
function validateAssetReadiness(profile, options) {
|
|
412
|
+
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
413
|
+
const sourceManifest = readAssetManifest(projectDir);
|
|
414
|
+
const manifest = filterAssetManifest(sourceManifest, options.assetIds);
|
|
415
|
+
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
416
|
+
const evidencePath = options.output ? resolve(projectDir, options.output) : undefined;
|
|
417
|
+
const validation = validateAssets({
|
|
418
|
+
projectDir,
|
|
419
|
+
noPlaceholders: options.noPlaceholders,
|
|
420
|
+
requireLicense: options.requireLicense,
|
|
421
|
+
provenanceFile: options.provenanceFile,
|
|
422
|
+
assetIds: options.assetIds
|
|
423
|
+
});
|
|
424
|
+
const externalProvenance = readExternalProvenance(projectDir, options.provenanceFile);
|
|
425
|
+
const failures = [...validation.failures];
|
|
426
|
+
const warnings = [...validation.warnings];
|
|
427
|
+
const modelAssets = manifest.assets.filter((asset) => asset.type === "model");
|
|
428
|
+
const animatedModels = modelAssets.filter((asset) => asset.animations.length > 0);
|
|
429
|
+
const animationClips = manifest.assets.reduce((total, asset) => total + asset.animations.length, 0);
|
|
430
|
+
const humanoidModels = modelAssets.filter((asset) => asset.humanoid?.humanoid).length;
|
|
431
|
+
const artifacts = createReadinessArtifacts(projectDir, manifest, evidencePath);
|
|
432
|
+
const assets = manifest.assets.map((asset) => {
|
|
433
|
+
const provenance = resolveAssetProvenance(asset, externalProvenance);
|
|
434
|
+
const placeholderFree = !isPlaceholderAsset(asset, provenance);
|
|
435
|
+
const licenseVerified = hasUsableLicenseEvidence(provenance);
|
|
436
|
+
const assetWarnings = [...asset.warnings];
|
|
437
|
+
const readinessIssues = createAssetReadinessIssues(profile, asset);
|
|
438
|
+
if (asset.type === "model" && !asset.bounds)
|
|
439
|
+
assetWarnings.push("Missing bounds; camera framing, collision proxies, and thumbnail composition will be weaker.");
|
|
440
|
+
if (asset.type === "model" && asset.materials.length === 0)
|
|
441
|
+
assetWarnings.push("No material names detected; authored visual diagnostics will be limited.");
|
|
442
|
+
if (asset.type === "model" && asset.sizeBytes > 50 * 1024 * 1024)
|
|
443
|
+
assetWarnings.push("Large model over 50MB; consider mesh/texture optimization before browser deployment.");
|
|
444
|
+
if (asset.type === "model" && asset.animations.length > 0 && asset.humanoid?.status === "unknown")
|
|
445
|
+
assetWarnings.push("Animated model has unknown humanoid status; inspect with --humanoid before using it as an acted character.");
|
|
446
|
+
assetWarnings.push(...readinessIssues.warnings);
|
|
447
|
+
failures.push(...readinessIssues.failures);
|
|
448
|
+
warnings.push(...readinessIssues.warnings);
|
|
449
|
+
const gameReady = asset.type === "model" && Boolean(asset.bounds) && asset.materials.length > 0 && asset.sizeBytes <= 50 * 1024 * 1024 && readinessIssues.failures.length === 0;
|
|
450
|
+
const cartoonReady = asset.type === "model"
|
|
451
|
+
? Boolean(asset.bounds) && (asset.animations.length > 0 || /prop|set|stage|background|environment/i.test(asset.id))
|
|
452
|
+
: asset.type === "audio" || asset.type === "texture";
|
|
453
|
+
const artifactPaths = artifacts.assetFiles.find((entry) => entry.id === asset.id) ?? createReadinessAssetArtifacts(projectDir, manifest, asset);
|
|
454
|
+
return {
|
|
455
|
+
id: asset.id,
|
|
456
|
+
type: asset.type,
|
|
457
|
+
format: asset.format,
|
|
458
|
+
source: asset.source,
|
|
459
|
+
outputPath: asset.outputPath,
|
|
460
|
+
url: asset.url,
|
|
461
|
+
sizeBytes: asset.sizeBytes,
|
|
462
|
+
bounds: asset.bounds,
|
|
463
|
+
boundsMetadata: asset.boundsMetadata,
|
|
464
|
+
animations: asset.animations,
|
|
465
|
+
animation: createReadinessAnimationMetadata(asset.animations),
|
|
466
|
+
animationMetadata: asset.animationMetadata,
|
|
467
|
+
humanoid: asset.humanoid,
|
|
468
|
+
skeleton: asset.skeleton,
|
|
469
|
+
morphTargets: asset.morphTargets,
|
|
470
|
+
provenance,
|
|
471
|
+
placeholderFree,
|
|
472
|
+
licenseVerified,
|
|
473
|
+
materials: asset.materials,
|
|
474
|
+
materialMetadata: asset.materialMetadata,
|
|
475
|
+
textures: asset.textures,
|
|
476
|
+
orientation: asset.orientation,
|
|
477
|
+
nodeNames: asset.nodeNames,
|
|
478
|
+
artifactPaths,
|
|
479
|
+
gameReady,
|
|
480
|
+
cartoonReady,
|
|
481
|
+
warnings: assetWarnings
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
if (profile === "game") {
|
|
485
|
+
if (modelAssets.length === 0)
|
|
486
|
+
failures.push("Game readiness requires at least one typed model asset. Add a GLB/GLTF with aura3d assets add ./fighter.glb --name fighter.");
|
|
487
|
+
if (animatedModels.length === 0)
|
|
488
|
+
warnings.push("No animated model clips detected. Static scenes can ship, but playable character showcases should include idle/walk/attack/hurt clips.");
|
|
489
|
+
if (animatedModels.length > 0 && humanoidModels === 0)
|
|
490
|
+
warnings.push("No humanoid model metadata detected. Character-heavy game routes should confirm humanoid status with assets inspect --humanoid and typed asset metadata.");
|
|
491
|
+
for (const asset of assets) {
|
|
492
|
+
if (asset.type !== "model")
|
|
493
|
+
continue;
|
|
494
|
+
if (!asset.gameReady)
|
|
495
|
+
warnings.push(`${asset.id}: not game-ready yet; expected bounds, named materials, and browser-sized payload.`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
if (modelAssets.length === 0)
|
|
500
|
+
failures.push("Cartoon readiness requires at least one typed model/set/prop GLB or GLTF.");
|
|
501
|
+
if (animatedModels.length === 0)
|
|
502
|
+
warnings.push("No animated character clips detected. Prompt-to-episode output can use transform animation, but character acting needs skeletal or pose clips.");
|
|
503
|
+
if (animatedModels.length > 0 && humanoidModels === 0)
|
|
504
|
+
warnings.push("No humanoid model metadata detected. Acting-heavy cartoon routes should confirm character rigs with assets inspect --humanoid.");
|
|
505
|
+
const audioAssets = manifest.assets.filter((asset) => asset.type === "audio");
|
|
506
|
+
if (audioAssets.length === 0)
|
|
507
|
+
warnings.push("No audio assets detected. AuraVoice bridge can still reference external narration manifests, but local episode proof is stronger with audio registered.");
|
|
508
|
+
}
|
|
509
|
+
const ok = failures.length === 0;
|
|
510
|
+
const baseMessage = ok
|
|
511
|
+
? `${profile === "game" ? "Game" : "Cartoon"} asset readiness report completed.`
|
|
512
|
+
: failures;
|
|
513
|
+
const messages = [
|
|
514
|
+
...(Array.isArray(baseMessage) ? baseMessage : [baseMessage]),
|
|
515
|
+
...(evidencePath ? [`Wrote asset readiness evidence: ${normalizeRelativePath(relative(projectDir, evidencePath))}`] : [])
|
|
516
|
+
];
|
|
517
|
+
const report = {
|
|
518
|
+
schema: "aura3d.asset-readiness/1.0",
|
|
519
|
+
profile,
|
|
520
|
+
ok,
|
|
521
|
+
status: ok ? "passed" : "failed",
|
|
522
|
+
validator: createReadinessValidatorEvidence(profile),
|
|
523
|
+
checkedAt: new Date().toISOString(),
|
|
524
|
+
manifestPath,
|
|
525
|
+
artifacts,
|
|
526
|
+
contracts: createReadinessValidationContracts(profile),
|
|
527
|
+
summary: {
|
|
528
|
+
totalAssets: manifest.assets.length,
|
|
529
|
+
modelAssets: modelAssets.length,
|
|
530
|
+
animatedModels: animatedModels.length,
|
|
531
|
+
textureAssets: manifest.assets.filter((asset) => asset.type === "texture").length,
|
|
532
|
+
audioAssets: manifest.assets.filter((asset) => asset.type === "audio").length,
|
|
533
|
+
environmentAssets: manifest.assets.filter((asset) => asset.type === "environment").length,
|
|
534
|
+
animationClips,
|
|
535
|
+
humanoidModels
|
|
536
|
+
},
|
|
537
|
+
assets,
|
|
538
|
+
failures,
|
|
539
|
+
warnings,
|
|
540
|
+
messages
|
|
541
|
+
};
|
|
542
|
+
if (evidencePath) {
|
|
543
|
+
mkdirSync(dirname(evidencePath), { recursive: true });
|
|
544
|
+
writeFileSync(evidencePath, `${JSON.stringify(report, null, 2)}\n`);
|
|
545
|
+
}
|
|
546
|
+
return report;
|
|
547
|
+
}
|
|
548
|
+
function filterAssetManifest(manifest, assetIds) {
|
|
549
|
+
const normalized = normalizeAssetIdFilter(assetIds);
|
|
550
|
+
if (normalized.length === 0)
|
|
551
|
+
return manifest;
|
|
552
|
+
const allowed = new Set(normalized);
|
|
553
|
+
return {
|
|
554
|
+
...manifest,
|
|
555
|
+
assets: manifest.assets.filter((asset) => allowed.has(asset.id))
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function findMissingAssetIds(manifest, assetIds) {
|
|
559
|
+
const normalized = normalizeAssetIdFilter(assetIds);
|
|
560
|
+
if (normalized.length === 0)
|
|
561
|
+
return [];
|
|
562
|
+
const existing = new Set(manifest.assets.map((asset) => asset.id));
|
|
563
|
+
return normalized.filter((id) => !existing.has(id));
|
|
564
|
+
}
|
|
565
|
+
function normalizeAssetIdFilter(assetIds) {
|
|
566
|
+
return [...new Set((assetIds ?? []).map((id) => id.trim()).filter(Boolean))];
|
|
567
|
+
}
|
|
568
|
+
function createReadinessValidatorEvidence(profile) {
|
|
569
|
+
return profile === "game"
|
|
570
|
+
? {
|
|
571
|
+
id: "aura-clash-game-assets",
|
|
572
|
+
command: "assets validate-game",
|
|
573
|
+
label: "AuraClash game asset validator"
|
|
574
|
+
}
|
|
575
|
+
: {
|
|
576
|
+
id: "aura-voice-cartoon-assets",
|
|
577
|
+
command: "assets validate-cartoon",
|
|
578
|
+
label: "AuraVoice cartoon asset validator"
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function createReadinessValidationContracts(profile) {
|
|
582
|
+
if (profile === "game") {
|
|
583
|
+
return [
|
|
584
|
+
{
|
|
585
|
+
id: "quaternius-game-ready-fighter-validation-contract",
|
|
586
|
+
label: "Quaternius-derived game-ready fighter validation contract",
|
|
587
|
+
profile: "game",
|
|
588
|
+
sourceFamily: "Quaternius",
|
|
589
|
+
intendedUse: "fighter",
|
|
590
|
+
sourceOnly: true,
|
|
591
|
+
requiredChecks: [
|
|
592
|
+
"typed Aura model asset entry generated by assets add",
|
|
593
|
+
"Quaternius provenance or source-family metadata",
|
|
594
|
+
"GLB/GLTF model with browser-sized payload",
|
|
595
|
+
"bounds with grounded pivot and fighter-scale dimensions",
|
|
596
|
+
"forward-facing +z or z orientation before runtime mirroring",
|
|
597
|
+
"humanoid skeleton metadata suitable for retarget diagnostics",
|
|
598
|
+
"readable visible materials and texture budget",
|
|
599
|
+
"thumbnail or first-frame artifact path",
|
|
600
|
+
"non-empty named fighting animation clips",
|
|
601
|
+
"no floating hair-only assembly without a body/head anchor"
|
|
602
|
+
],
|
|
603
|
+
requiredAnimationClips: ["idle", "walk", "lightPunch"],
|
|
604
|
+
evidenceBoundary: "This CLI contract is source-only. It does not prove a Quaternius fighter passed validation until assets validate-game output and retained runtime/browser evidence are archived."
|
|
605
|
+
}
|
|
606
|
+
];
|
|
607
|
+
}
|
|
608
|
+
return [
|
|
609
|
+
{
|
|
610
|
+
id: "auravoice-cartoon-character-asset-validation-contract",
|
|
611
|
+
label: "AuraVoice cartoon asset validation contract",
|
|
612
|
+
profile: "cartoon",
|
|
613
|
+
sourceFamily: "AuraVoice",
|
|
614
|
+
intendedUse: "cartoon-character",
|
|
615
|
+
sourceOnly: true,
|
|
616
|
+
requiredChecks: [
|
|
617
|
+
"typed Aura model, texture, audio, or environment asset entry",
|
|
618
|
+
"bounds for model/set composition",
|
|
619
|
+
"animation or transform-ready character metadata",
|
|
620
|
+
"audio or external AuraVoice manifest references for stronger episode proof"
|
|
621
|
+
],
|
|
622
|
+
evidenceBoundary: "This CLI contract is source-only. It does not prove cartoon route readiness until validate-cartoon output, rendered frames, timing proof, and AuraVoice evidence are archived."
|
|
623
|
+
}
|
|
624
|
+
];
|
|
625
|
+
}
|
|
626
|
+
function createReadinessArtifacts(projectDir, manifest, evidencePath) {
|
|
627
|
+
const artifacts = {
|
|
628
|
+
manifestPath: resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
|
|
629
|
+
typedAssetsPath: resolve(projectDir, manifest.typegen),
|
|
630
|
+
outputDir: resolve(projectDir, manifest.outputDir),
|
|
631
|
+
assetBasePath: manifest.assetBasePath,
|
|
632
|
+
assetFiles: manifest.assets.map((asset) => createReadinessAssetArtifacts(projectDir, manifest, asset))
|
|
633
|
+
};
|
|
634
|
+
if (evidencePath)
|
|
635
|
+
artifacts.evidencePath = evidencePath;
|
|
636
|
+
return artifacts;
|
|
637
|
+
}
|
|
638
|
+
function createReadinessAssetArtifacts(projectDir, manifest, asset) {
|
|
639
|
+
const artifact = {
|
|
640
|
+
id: asset.id,
|
|
641
|
+
sourcePath: resolve(projectDir, asset.source),
|
|
642
|
+
outputPath: resolve(projectDir, asset.outputPath),
|
|
643
|
+
publicUrl: asset.url,
|
|
644
|
+
dependencyPaths: (asset.dependencies ?? []).map((dependency) => resolve(dirname(resolve(projectDir, asset.outputPath)), dependency))
|
|
645
|
+
};
|
|
646
|
+
if (asset.thumbnailUrl) {
|
|
647
|
+
artifact.thumbnailUrl = asset.thumbnailUrl;
|
|
648
|
+
const thumbnailPath = resolvePublicArtifactPath(projectDir, manifest, asset.thumbnailUrl);
|
|
649
|
+
if (thumbnailPath)
|
|
650
|
+
artifact.thumbnailPath = thumbnailPath;
|
|
651
|
+
}
|
|
652
|
+
return artifact;
|
|
653
|
+
}
|
|
654
|
+
function createReadinessAnimationMetadata(animations) {
|
|
655
|
+
return {
|
|
656
|
+
clipCount: animations.length,
|
|
657
|
+
clips: animations.map((name, index) => ({ index, name }))
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function createAssetReadinessIssues(profile, asset) {
|
|
661
|
+
if (asset.type !== "model")
|
|
662
|
+
return { failures: [], warnings: [] };
|
|
663
|
+
const failures = [];
|
|
664
|
+
const warnings = [];
|
|
665
|
+
const prefix = `${asset.id}:`;
|
|
666
|
+
const characterLike = isCharacterLikeAsset(asset);
|
|
667
|
+
if (profile === "game" && characterLike) {
|
|
668
|
+
const missing = missingRequiredGameClips(asset.animations);
|
|
669
|
+
if (missing.length > 0) {
|
|
670
|
+
failures.push(`${prefix} missing required game animation clip${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}.`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const emptyClips = (asset.animationMetadata?.clips ?? []).filter((clip) => clip.channelCount === 0 || clip.samplerCount === 0);
|
|
674
|
+
for (const clip of emptyClips) {
|
|
675
|
+
failures.push(`${prefix} animation clip "${clip.name}" is empty; expected at least one channel and sampler.`);
|
|
676
|
+
}
|
|
677
|
+
const bounds = asset.boundsMetadata;
|
|
678
|
+
if (bounds && bounds.maxDimension > 50) {
|
|
679
|
+
failures.push(`${prefix} oversized bounds detected; largest dimension is ${bounds.maxDimension}m, expected at most 50m for browser game assets.`);
|
|
680
|
+
}
|
|
681
|
+
else if (bounds && characterLike && bounds.maxDimension > 4) {
|
|
682
|
+
warnings.push(`${prefix} character-sized model is unusually large (${bounds.maxDimension}m); confirm scale before using it in gameplay.`);
|
|
683
|
+
}
|
|
684
|
+
if (bounds && characterLike && !bounds.grounded) {
|
|
685
|
+
warnings.push(`${prefix} bounds are not grounded at the pivot; min.y is ${bounds.min[1]}m.`);
|
|
686
|
+
}
|
|
687
|
+
const orientation = asset.orientation;
|
|
688
|
+
if (profile === "game" && characterLike && orientation?.forwardAxis && !["+z", "z"].includes(orientation.forwardAxis.toLowerCase())) {
|
|
689
|
+
failures.push(`${prefix} wrong facing direction "${orientation.forwardAxis}"; fighting-game characters are expected to face +z before runtime mirroring.`);
|
|
690
|
+
}
|
|
691
|
+
const invisibleMaterials = (asset.materialMetadata ?? []).filter((material) => !material.visible || !material.readable);
|
|
692
|
+
for (const material of invisibleMaterials) {
|
|
693
|
+
failures.push(`${prefix} invisible or unreadable material "${material.name}" detected${material.reasons.length ? ` (${material.reasons.join("; ")})` : ""}.`);
|
|
694
|
+
}
|
|
695
|
+
if (profile === "game" && hasFloatingHairRisk(asset)) {
|
|
696
|
+
failures.push(`${prefix} floating hair risk detected; hair-only geometry must be assembled onto a body/head with assets assemble-character before game validation.`);
|
|
697
|
+
}
|
|
698
|
+
return { failures, warnings };
|
|
699
|
+
}
|
|
700
|
+
function isCharacterLikeAsset(asset) {
|
|
701
|
+
if (asset.humanoid?.humanoid)
|
|
702
|
+
return true;
|
|
703
|
+
return /fighter|player|opponent|enemy|hero|character|avatar|humanoid|npc|body|mara/i.test(asset.id);
|
|
704
|
+
}
|
|
705
|
+
function missingRequiredGameClips(animations) {
|
|
706
|
+
const normalized = animations.map((name) => name.toLowerCase().replace(/[^a-z0-9]/g, ""));
|
|
707
|
+
const hasNamed = (patterns) => normalized.some((name) => patterns.some((pattern) => pattern.test(name)));
|
|
708
|
+
const missing = [];
|
|
709
|
+
if (!hasNamed([/idle/, /stand/]))
|
|
710
|
+
missing.push("idle");
|
|
711
|
+
if (!hasNamed([/walk/, /locomotion/, /move/]))
|
|
712
|
+
missing.push("walk");
|
|
713
|
+
if (!hasNamed([/lightpunch/, /lightattack/, /light/, /jab/, /punch/, /attack/]))
|
|
714
|
+
missing.push("lightPunch");
|
|
715
|
+
return missing;
|
|
716
|
+
}
|
|
717
|
+
function hasFloatingHairRisk(asset) {
|
|
718
|
+
const names = [asset.id, ...(asset.nodeNames ?? [])].join(" ").toLowerCase();
|
|
719
|
+
if (!names.includes("hair"))
|
|
720
|
+
return false;
|
|
721
|
+
const hasBodyAnchor = /body|torso|spine|chest|head|neck|skull|face|hips|pelvis/.test(names);
|
|
722
|
+
return !hasBodyAnchor;
|
|
723
|
+
}
|
|
724
|
+
function resolvePublicArtifactPath(projectDir, manifest, url) {
|
|
725
|
+
if (/^https?:\/\//i.test(url))
|
|
726
|
+
return undefined;
|
|
727
|
+
if (url.startsWith(manifest.assetBasePath)) {
|
|
728
|
+
return resolve(projectDir, manifest.outputDir, url.slice(manifest.assetBasePath.length));
|
|
729
|
+
}
|
|
730
|
+
return resolve(projectDir, url.replace(/^\//, "public/"));
|
|
731
|
+
}
|
|
732
|
+
function defaultAttachPoint(slot) {
|
|
733
|
+
const normalized = slot.toLowerCase();
|
|
734
|
+
if (normalized.includes("hair") || normalized.includes("hat") || normalized.includes("face"))
|
|
735
|
+
return "head";
|
|
736
|
+
if (normalized.includes("hand") || normalized.includes("weapon") || normalized.includes("prop"))
|
|
737
|
+
return "rightHand";
|
|
738
|
+
if (normalized.includes("shoe") || normalized.includes("boot"))
|
|
739
|
+
return "feet";
|
|
740
|
+
if (normalized.includes("cape") || normalized.includes("back"))
|
|
741
|
+
return "spine";
|
|
742
|
+
return "root";
|
|
743
|
+
}
|
|
744
|
+
export function readAssetManifest(projectDir) {
|
|
745
|
+
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
746
|
+
if (!existsSync(manifestPath)) {
|
|
747
|
+
return {
|
|
748
|
+
schema: "aura3d.assets/1.0",
|
|
749
|
+
assetBasePath: DEFAULT_AURA_ASSET_PUBLIC_PATH,
|
|
750
|
+
outputDir: DEFAULT_AURA_ASSET_OUTPUT_DIR,
|
|
751
|
+
typegen: DEFAULT_AURA_ASSET_TYPEGEN,
|
|
752
|
+
assets: []
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
756
|
+
if (parsed.schema !== "aura3d.assets/1.0")
|
|
757
|
+
throw new Error(`Unsupported Aura3D asset manifest schema: ${String(parsed.schema)}`);
|
|
758
|
+
return parsed;
|
|
759
|
+
}
|
|
760
|
+
export function writeAssetManifest(projectDir, manifest) {
|
|
761
|
+
writeFileSync(resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
762
|
+
}
|
|
763
|
+
function inspectAssetFile(path, format) {
|
|
764
|
+
if (format === "gltf")
|
|
765
|
+
return inspectGltf(JSON.parse(readFileSync(path, "utf8")), dirname(path));
|
|
766
|
+
if (format === "glb")
|
|
767
|
+
return inspectGlb(readFileSync(path), dirname(path));
|
|
768
|
+
return {
|
|
769
|
+
materials: [],
|
|
770
|
+
materialMetadata: [],
|
|
771
|
+
animations: [],
|
|
772
|
+
animation: emptyAnimationInspection(),
|
|
773
|
+
humanoid: unknownHumanoidInspection("Humanoid detection is only available for GLB/glTF model assets."),
|
|
774
|
+
skeleton: emptySkeletonInspection("Skeleton detection is only available for GLB/glTF model assets."),
|
|
775
|
+
morphTargets: emptyMorphTargetInspection("Morph target detection is only available for GLB/glTF model assets."),
|
|
776
|
+
textures: [],
|
|
777
|
+
orientation: unknownOrientationInspection(),
|
|
778
|
+
nodeNames: [],
|
|
779
|
+
dependencies: [],
|
|
780
|
+
bounds: undefined
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function inspectGlb(buffer, baseDir) {
|
|
784
|
+
if (buffer.toString("utf8", 0, 4) !== "glTF")
|
|
785
|
+
throw new Error("Invalid GLB header. Suggested fix: re-export the asset as binary glTF (.glb).");
|
|
786
|
+
const length = buffer.readUInt32LE(8);
|
|
787
|
+
if (length > buffer.length)
|
|
788
|
+
throw new Error("Invalid GLB length. Suggested fix: run assets validate on the original export.");
|
|
789
|
+
const chunkLength = buffer.readUInt32LE(12);
|
|
790
|
+
const chunkType = buffer.toString("utf8", 16, 20);
|
|
791
|
+
if (chunkType !== "JSON")
|
|
792
|
+
throw new Error("Invalid GLB JSON chunk. Suggested fix: re-export the GLB.");
|
|
793
|
+
const json = JSON.parse(buffer.toString("utf8", 20, 20 + chunkLength).trim());
|
|
794
|
+
return inspectGltf(json, baseDir);
|
|
795
|
+
}
|
|
796
|
+
function inspectGltf(json, baseDir) {
|
|
797
|
+
const dependencies = [
|
|
798
|
+
...(json.images ?? []).map((image) => image.uri).filter(isExternalUri),
|
|
799
|
+
...(json.buffers ?? []).map((buffer) => buffer.uri).filter(isExternalUri)
|
|
800
|
+
];
|
|
801
|
+
if (baseDir) {
|
|
802
|
+
const missing = dependencies.filter((dependency) => !existsSync(resolve(baseDir, dependency)));
|
|
803
|
+
if (missing.length > 0) {
|
|
804
|
+
throw new Error(`Aura3D assets add failed: referenced asset file missing: ${missing.join(", ")}. Suggested fix: keep external .bin and texture files beside the .gltf or export as .glb.`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const boundsMetadata = extractBoundsDetails(json);
|
|
808
|
+
return {
|
|
809
|
+
bounds: boundsMetadata?.size,
|
|
810
|
+
boundsMetadata,
|
|
811
|
+
materials: (json.materials ?? []).map((material, index) => material.name ?? `material-${index}`),
|
|
812
|
+
materialMetadata: inspectGltfMaterials(json),
|
|
813
|
+
animations: (json.animations ?? []).map((animation, index) => animation.name ?? `clip-${index}`),
|
|
814
|
+
animation: inspectGltfAnimations(json),
|
|
815
|
+
humanoid: inspectGltfHumanoid(json),
|
|
816
|
+
skeleton: inspectGltfSkeleton(json),
|
|
817
|
+
morphTargets: inspectGltfMorphTargets(json),
|
|
818
|
+
provenance: inspectGltfProvenance(json),
|
|
819
|
+
textures: (json.images ?? []).map((image, index) => image.uri ?? image.name ?? `image-${index}`),
|
|
820
|
+
orientation: inspectGltfOrientation(json),
|
|
821
|
+
nodeNames: (json.nodes ?? []).map((node, index) => node.name ?? `node-${index}`),
|
|
822
|
+
dependencies
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function emptyAnimationInspection() {
|
|
826
|
+
return {
|
|
827
|
+
clipCount: 0,
|
|
828
|
+
clips: [],
|
|
829
|
+
messages: ["No embedded animation clips detected."]
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function inspectGltfAnimations(json) {
|
|
833
|
+
const clips = (json.animations ?? []).map((animation, index) => {
|
|
834
|
+
const channels = animation.channels ?? [];
|
|
835
|
+
const targetPaths = uniqueStrings(channels.map((channel) => channel.target?.path).filter(isString));
|
|
836
|
+
const targetNodes = uniqueStrings(channels.map((channel) => {
|
|
837
|
+
const nodeIndex = channel.target?.node;
|
|
838
|
+
return typeof nodeIndex === "number" ? json.nodes?.[nodeIndex]?.name ?? `node-${nodeIndex}` : undefined;
|
|
839
|
+
}).filter(isString));
|
|
840
|
+
return {
|
|
841
|
+
index,
|
|
842
|
+
name: animation.name ?? `clip-${index}`,
|
|
843
|
+
channelCount: channels.length,
|
|
844
|
+
samplerCount: animation.samplers?.length ?? 0,
|
|
845
|
+
targetPaths,
|
|
846
|
+
targetNodes
|
|
847
|
+
};
|
|
848
|
+
});
|
|
849
|
+
return {
|
|
850
|
+
clipCount: clips.length,
|
|
851
|
+
clips,
|
|
852
|
+
messages: clips.length === 0
|
|
853
|
+
? ["No embedded animation clips detected."]
|
|
854
|
+
: [`Detected ${clips.length} embedded animation clip${clips.length === 1 ? "" : "s"}.`]
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
function inspectGltfHumanoid(json) {
|
|
858
|
+
const skinCount = json.skins?.length ?? 0;
|
|
859
|
+
const jointIndexes = uniqueNumbers((json.skins ?? []).flatMap((skin) => skin.joints ?? []));
|
|
860
|
+
const jointNames = uniqueStrings(jointIndexes.map((index) => json.nodes?.[index]?.name ?? `joint-${index}`));
|
|
861
|
+
const nodeNames = uniqueStrings((json.nodes ?? []).map((node, index) => node.name ?? `node-${index}`));
|
|
862
|
+
const candidates = jointNames.length > 0 ? jointNames : nodeNames;
|
|
863
|
+
const requiredBones = ["hips", "spine", "head", "leftArm", "rightArm", "leftLeg", "rightLeg"];
|
|
864
|
+
const matchedBones = requiredBones.filter((bone) => candidates.some((name) => matchesHumanoidBone(name, bone)));
|
|
865
|
+
const missingBones = requiredBones.filter((bone) => !matchedBones.includes(bone));
|
|
866
|
+
const hasSkin = jointIndexes.length > 0;
|
|
867
|
+
const hasTorso = matchedBones.includes("hips") && matchedBones.includes("spine") && matchedBones.includes("head");
|
|
868
|
+
const hasArms = matchedBones.includes("leftArm") && matchedBones.includes("rightArm");
|
|
869
|
+
const hasLegs = matchedBones.includes("leftLeg") && matchedBones.includes("rightLeg");
|
|
870
|
+
const humanoid = (hasSkin && hasTorso && hasArms && hasLegs) || (!hasSkin && hasTorso && matchedBones.length >= 5);
|
|
871
|
+
const status = humanoid
|
|
872
|
+
? "humanoid"
|
|
873
|
+
: hasSkin || matchedBones.length > 0
|
|
874
|
+
? "unknown"
|
|
875
|
+
: "non-humanoid";
|
|
876
|
+
const confidence = humanoid && hasSkin
|
|
877
|
+
? "high"
|
|
878
|
+
: humanoid || (hasSkin && matchedBones.length >= 5)
|
|
879
|
+
? "medium"
|
|
880
|
+
: "low";
|
|
881
|
+
return {
|
|
882
|
+
humanoid,
|
|
883
|
+
status,
|
|
884
|
+
confidence,
|
|
885
|
+
skinCount,
|
|
886
|
+
jointCount: jointIndexes.length,
|
|
887
|
+
matchedBones,
|
|
888
|
+
missingBones,
|
|
889
|
+
messages: humanoid
|
|
890
|
+
? [`Humanoid signals detected from ${hasSkin ? "skinned joints" : "node names"}.`]
|
|
891
|
+
: status === "unknown"
|
|
892
|
+
? [`Humanoid status is unknown; missing bone groups: ${missingBones.join(", ")}.`]
|
|
893
|
+
: ["No humanoid skeleton signals detected."]
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function inspectGltfSkeleton(json) {
|
|
897
|
+
const skins = (json.skins ?? []).map((skin, index) => {
|
|
898
|
+
const joints = (skin.joints ?? []).map((jointIndex) => json.nodes?.[jointIndex]?.name ?? `joint-${jointIndex}`);
|
|
899
|
+
const skeletonIndex = skin.skeleton;
|
|
900
|
+
return {
|
|
901
|
+
index,
|
|
902
|
+
name: skin.name ?? `skin-${index}`,
|
|
903
|
+
jointCount: joints.length,
|
|
904
|
+
joints,
|
|
905
|
+
...(typeof skeletonIndex === "number" ? { skeleton: json.nodes?.[skeletonIndex]?.name ?? `node-${skeletonIndex}` } : {})
|
|
906
|
+
};
|
|
907
|
+
});
|
|
908
|
+
const jointCount = uniqueStrings(skins.flatMap((skin) => skin.joints)).length;
|
|
909
|
+
return {
|
|
910
|
+
skinCount: skins.length,
|
|
911
|
+
jointCount,
|
|
912
|
+
skins,
|
|
913
|
+
messages: skins.length === 0
|
|
914
|
+
? ["No skin/skeleton metadata detected."]
|
|
915
|
+
: [`Detected ${skins.length} skin${skins.length === 1 ? "" : "s"} with ${jointCount} unique joint${jointCount === 1 ? "" : "s"}.`]
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function emptySkeletonInspection(message) {
|
|
919
|
+
return {
|
|
920
|
+
skinCount: 0,
|
|
921
|
+
jointCount: 0,
|
|
922
|
+
skins: [],
|
|
923
|
+
messages: [message]
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function inspectGltfMorphTargets(json) {
|
|
927
|
+
const meshes = (json.meshes ?? []).map((mesh, index) => {
|
|
928
|
+
const meshExtras = objectValue(mesh.extras);
|
|
929
|
+
const namedTargets = stringArrayValue(meshExtras?.targetNames ?? meshExtras?.morphTargetNames);
|
|
930
|
+
const targetCount = Math.max(namedTargets.length, mesh.weights?.length ?? 0, ...(mesh.primitives ?? []).map((primitive) => primitive.targets?.length ?? 0));
|
|
931
|
+
if (targetCount === 0)
|
|
932
|
+
return undefined;
|
|
933
|
+
const targetNames = targetCount > 0
|
|
934
|
+
? Array.from({ length: targetCount }, (_, targetIndex) => namedTargets[targetIndex] ?? `target-${targetIndex}`)
|
|
935
|
+
: [];
|
|
936
|
+
return {
|
|
937
|
+
index,
|
|
938
|
+
name: mesh.name ?? `mesh-${index}`,
|
|
939
|
+
targetNames
|
|
940
|
+
};
|
|
941
|
+
}).filter((mesh) => Boolean(mesh));
|
|
942
|
+
const targetNames = uniqueStrings(meshes.flatMap((mesh) => mesh.targetNames));
|
|
943
|
+
return {
|
|
944
|
+
targetCount: targetNames.length,
|
|
945
|
+
targetNames,
|
|
946
|
+
meshes,
|
|
947
|
+
messages: targetNames.length === 0
|
|
948
|
+
? ["No morph target metadata detected."]
|
|
949
|
+
: [`Detected ${targetNames.length} morph target${targetNames.length === 1 ? "" : "s"}.`]
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function emptyMorphTargetInspection(message) {
|
|
953
|
+
return {
|
|
954
|
+
targetCount: 0,
|
|
955
|
+
targetNames: [],
|
|
956
|
+
meshes: [],
|
|
957
|
+
messages: [message]
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function inspectGltfProvenance(json) {
|
|
961
|
+
const assetExtras = objectValue(json.asset?.extras);
|
|
962
|
+
const auraExtras = objectValue(assetExtras?.aura3d) ?? assetExtras;
|
|
963
|
+
const provenance = objectValue(auraExtras?.provenance ?? auraExtras?.license ?? auraExtras?.source);
|
|
964
|
+
const sourceUrl = stringValue(provenance?.sourceUrl ?? provenance?.url ?? auraExtras?.sourceUrl);
|
|
965
|
+
const license = stringValue(provenance?.license ?? provenance?.spdx ?? auraExtras?.license);
|
|
966
|
+
const author = stringValue(provenance?.author ?? provenance?.creator ?? auraExtras?.author);
|
|
967
|
+
const sourceFamily = stringValue(provenance?.sourceFamily ?? provenance?.source ?? auraExtras?.sourceFamily);
|
|
968
|
+
const attribution = stringValue(provenance?.attribution ?? auraExtras?.attribution);
|
|
969
|
+
const evidence = stringArrayValue(provenance?.evidence ?? auraExtras?.evidence);
|
|
970
|
+
if (!sourceUrl && !license && !author && !sourceFamily && !attribution && evidence.length === 0)
|
|
971
|
+
return undefined;
|
|
972
|
+
return {
|
|
973
|
+
...(sourceUrl ? { sourceUrl } : {}),
|
|
974
|
+
...(license ? { license } : {}),
|
|
975
|
+
...(author ? { author } : {}),
|
|
976
|
+
...(sourceFamily ? { sourceFamily } : {}),
|
|
977
|
+
...(attribution ? { attribution } : {}),
|
|
978
|
+
...(evidence.length > 0 ? { evidence } : {})
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function unknownHumanoidInspection(message) {
|
|
982
|
+
return {
|
|
983
|
+
humanoid: false,
|
|
984
|
+
status: "unknown",
|
|
985
|
+
confidence: "low",
|
|
986
|
+
skinCount: 0,
|
|
987
|
+
jointCount: 0,
|
|
988
|
+
matchedBones: [],
|
|
989
|
+
missingBones: ["hips", "spine", "head", "leftArm", "rightArm", "leftLeg", "rightLeg"],
|
|
990
|
+
messages: [message]
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function matchesHumanoidBone(name, bone) {
|
|
994
|
+
const raw = name.toLowerCase();
|
|
995
|
+
const compact = raw.replace(/[^a-z0-9]/g, "");
|
|
996
|
+
const left = compact.includes("left") || /(^|[^a-z0-9])l([^a-z0-9]|$)/.test(raw) || /[^a-z0-9]l$/.test(raw);
|
|
997
|
+
const right = compact.includes("right") || /(^|[^a-z0-9])r([^a-z0-9]|$)/.test(raw) || /[^a-z0-9]r$/.test(raw);
|
|
998
|
+
const arm = compact.includes("arm") || compact.includes("forearm") || compact.includes("shoulder") || compact.includes("hand");
|
|
999
|
+
const leg = compact.includes("leg") || compact.includes("thigh") || compact.includes("foot") || compact.includes("toe");
|
|
1000
|
+
if (bone === "hips")
|
|
1001
|
+
return compact.includes("hip") || compact.includes("pelvis");
|
|
1002
|
+
if (bone === "spine")
|
|
1003
|
+
return compact.includes("spine") || compact.includes("chest") || compact.includes("torso");
|
|
1004
|
+
if (bone === "head")
|
|
1005
|
+
return compact.includes("head") || compact.includes("neck");
|
|
1006
|
+
if (bone === "leftArm")
|
|
1007
|
+
return left && arm;
|
|
1008
|
+
if (bone === "rightArm")
|
|
1009
|
+
return right && arm;
|
|
1010
|
+
if (bone === "leftLeg")
|
|
1011
|
+
return left && leg;
|
|
1012
|
+
return right && leg;
|
|
1013
|
+
}
|
|
1014
|
+
function isExternalUri(uri) {
|
|
1015
|
+
return typeof uri === "string" && uri.length > 0 && !uri.startsWith("data:");
|
|
1016
|
+
}
|
|
1017
|
+
function isString(value) {
|
|
1018
|
+
return typeof value === "string";
|
|
1019
|
+
}
|
|
1020
|
+
function uniqueStrings(values) {
|
|
1021
|
+
return [...new Set(values)];
|
|
1022
|
+
}
|
|
1023
|
+
function uniqueNumbers(values) {
|
|
1024
|
+
return [...new Set(values)];
|
|
1025
|
+
}
|
|
1026
|
+
function extractBoundsDetails(json) {
|
|
1027
|
+
let min;
|
|
1028
|
+
let max;
|
|
1029
|
+
for (const accessor of json.accessors ?? []) {
|
|
1030
|
+
if (!accessor.min || !accessor.max || accessor.min.length < 3 || accessor.max.length < 3)
|
|
1031
|
+
continue;
|
|
1032
|
+
min = min ? [Math.min(min[0], accessor.min[0]), Math.min(min[1], accessor.min[1]), Math.min(min[2], accessor.min[2])] : [accessor.min[0], accessor.min[1], accessor.min[2]];
|
|
1033
|
+
max = max ? [Math.max(max[0], accessor.max[0]), Math.max(max[1], accessor.max[1]), Math.max(max[2], accessor.max[2])] : [accessor.max[0], accessor.max[1], accessor.max[2]];
|
|
1034
|
+
}
|
|
1035
|
+
if (!min || !max)
|
|
1036
|
+
return undefined;
|
|
1037
|
+
const size = [round(max[0] - min[0]), round(max[1] - min[1]), round(max[2] - min[2])];
|
|
1038
|
+
const center = [round((min[0] + max[0]) / 2), round((min[1] + max[1]) / 2), round((min[2] + max[2]) / 2)];
|
|
1039
|
+
const roundedMin = [round(min[0]), round(min[1]), round(min[2])];
|
|
1040
|
+
const roundedMax = [round(max[0]), round(max[1]), round(max[2])];
|
|
1041
|
+
return {
|
|
1042
|
+
min: roundedMin,
|
|
1043
|
+
max: roundedMax,
|
|
1044
|
+
size,
|
|
1045
|
+
center,
|
|
1046
|
+
maxDimension: Math.max(...size),
|
|
1047
|
+
grounded: Math.abs(roundedMin[1]) <= 0.08
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
function inspectGltfMaterials(json) {
|
|
1051
|
+
return (json.materials ?? []).map((material, index) => {
|
|
1052
|
+
const name = material.name ?? `material-${index}`;
|
|
1053
|
+
const extras = objectValue(material.extras);
|
|
1054
|
+
const explicitVisible = booleanValue(extras?.visible);
|
|
1055
|
+
const explicitReadable = booleanValue(extras?.readable);
|
|
1056
|
+
const opacity = round(numberValue(material.pbrMetallicRoughness?.baseColorFactor?.[3]) ?? 1);
|
|
1057
|
+
const alphaMode = material.alphaMode;
|
|
1058
|
+
const alphaCutoff = numberValue(material.alphaCutoff);
|
|
1059
|
+
const reasons = [];
|
|
1060
|
+
if (opacity <= 0)
|
|
1061
|
+
reasons.push("baseColorFactor alpha is 0");
|
|
1062
|
+
if (alphaMode === "MASK" && (alphaCutoff ?? 0.5) >= 1)
|
|
1063
|
+
reasons.push("alpha mask cutoff hides fully transparent surfaces");
|
|
1064
|
+
if (explicitVisible === false)
|
|
1065
|
+
reasons.push("material extras mark visible=false");
|
|
1066
|
+
if (explicitReadable === false)
|
|
1067
|
+
reasons.push("material extras mark readable=false");
|
|
1068
|
+
const visible = explicitVisible ?? (opacity > 0 && !(alphaMode === "MASK" && (alphaCutoff ?? 0.5) >= 1));
|
|
1069
|
+
const readable = explicitReadable ?? visible;
|
|
1070
|
+
return {
|
|
1071
|
+
name,
|
|
1072
|
+
visible,
|
|
1073
|
+
readable,
|
|
1074
|
+
opacity,
|
|
1075
|
+
...(alphaMode ? { alphaMode } : {}),
|
|
1076
|
+
reasons
|
|
1077
|
+
};
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
function inspectGltfOrientation(json) {
|
|
1081
|
+
const assetExtras = objectValue(json.asset?.extras);
|
|
1082
|
+
const auraExtras = objectValue(assetExtras?.aura3d) ?? assetExtras;
|
|
1083
|
+
const orientation = objectValue(auraExtras?.orientation) ?? auraExtras;
|
|
1084
|
+
const forwardAxis = stringValue(orientation?.forwardAxis ?? orientation?.forward ?? orientation?.facing);
|
|
1085
|
+
const upAxis = stringValue(orientation?.upAxis ?? orientation?.up);
|
|
1086
|
+
if (!forwardAxis && !upAxis)
|
|
1087
|
+
return unknownOrientationInspection();
|
|
1088
|
+
return {
|
|
1089
|
+
source: "gltf-extras",
|
|
1090
|
+
...(forwardAxis ? { forwardAxis } : {}),
|
|
1091
|
+
...(upAxis ? { upAxis } : {}),
|
|
1092
|
+
messages: [`Orientation metadata detected${forwardAxis ? ` with forwardAxis=${forwardAxis}` : ""}${upAxis ? ` and upAxis=${upAxis}` : ""}.`]
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function unknownOrientationInspection() {
|
|
1096
|
+
return {
|
|
1097
|
+
source: "unknown",
|
|
1098
|
+
messages: ["No orientation metadata detected; facing direction cannot be proven."]
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function objectValue(value) {
|
|
1102
|
+
return typeof value === "object" && value !== null ? value : undefined;
|
|
1103
|
+
}
|
|
1104
|
+
function stringValue(value) {
|
|
1105
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1106
|
+
}
|
|
1107
|
+
function stringArrayValue(value) {
|
|
1108
|
+
if (!Array.isArray(value))
|
|
1109
|
+
return [];
|
|
1110
|
+
return value.map((entry) => stringValue(entry)).filter((entry) => Boolean(entry));
|
|
1111
|
+
}
|
|
1112
|
+
function booleanValue(value) {
|
|
1113
|
+
return typeof value === "boolean" ? value : undefined;
|
|
1114
|
+
}
|
|
1115
|
+
function numberValue(value) {
|
|
1116
|
+
const number = Number(value);
|
|
1117
|
+
return Number.isFinite(number) ? number : undefined;
|
|
1118
|
+
}
|
|
1119
|
+
function createAssetWarnings(path, inspection) {
|
|
1120
|
+
const warnings = [];
|
|
1121
|
+
const size = statSync(path).size;
|
|
1122
|
+
if (size > 25 * 1024 * 1024)
|
|
1123
|
+
warnings.push("asset exceeds 25 MB; consider compression before deployment");
|
|
1124
|
+
if (!inspection.bounds)
|
|
1125
|
+
warnings.push("bounds could not be extracted");
|
|
1126
|
+
if (inspection.textures.length === 0 && ["glb", "gltf"].includes(extname(path).slice(1).toLowerCase()))
|
|
1127
|
+
warnings.push("no texture references detected");
|
|
1128
|
+
if (inspection.orientation.source === "unknown" && ["glb", "gltf"].includes(extname(path).slice(1).toLowerCase()))
|
|
1129
|
+
warnings.push("orientation metadata missing; facing direction cannot be validated until GLTF extras declare aura3d.orientation.forwardAxis");
|
|
1130
|
+
if (inspection.materialMetadata.some((material) => !material.visible || !material.readable))
|
|
1131
|
+
warnings.push("one or more materials are invisible or unreadable");
|
|
1132
|
+
return warnings;
|
|
1133
|
+
}
|
|
1134
|
+
function createAssetProvenance(projectDir, sourcePath, options, detected) {
|
|
1135
|
+
return {
|
|
1136
|
+
sourcePath: normalizeRelativePath(relative(projectDir, sourcePath)),
|
|
1137
|
+
...(options.sourceUrl ?? detected?.sourceUrl ? { sourceUrl: options.sourceUrl ?? detected?.sourceUrl } : {}),
|
|
1138
|
+
...(options.license ?? detected?.license ? { license: options.license ?? detected?.license } : {}),
|
|
1139
|
+
...(options.author ?? detected?.author ? { author: options.author ?? detected?.author } : {}),
|
|
1140
|
+
...(options.sourceFamily ?? detected?.sourceFamily ? { sourceFamily: options.sourceFamily ?? detected?.sourceFamily } : {}),
|
|
1141
|
+
...(options.attribution ?? detected?.attribution ? { attribution: options.attribution ?? detected?.attribution } : {}),
|
|
1142
|
+
...(detected?.evidence && detected.evidence.length > 0 ? { evidence: detected.evidence } : {}),
|
|
1143
|
+
checkedAt: new Date().toISOString()
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function readExternalProvenance(projectDir, provenanceFile) {
|
|
1147
|
+
if (!provenanceFile)
|
|
1148
|
+
return new Map();
|
|
1149
|
+
const path = resolve(projectDir, provenanceFile);
|
|
1150
|
+
if (!existsSync(path))
|
|
1151
|
+
return new Map();
|
|
1152
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1153
|
+
const root = objectValue(parsed);
|
|
1154
|
+
if (!root)
|
|
1155
|
+
return new Map();
|
|
1156
|
+
const checkedAt = stringValue(root.updatedAt ?? root.verifiedAt ?? root.checkedAt) ?? new Date().toISOString();
|
|
1157
|
+
const records = [
|
|
1158
|
+
...arrayObjectValue(root.launchGlbs),
|
|
1159
|
+
...arrayObjectValue(root.assets),
|
|
1160
|
+
...arrayObjectValue(root.assetEvidence)
|
|
1161
|
+
];
|
|
1162
|
+
const byId = new Map();
|
|
1163
|
+
for (const record of records) {
|
|
1164
|
+
const typedAsset = stringValue(record.typedAsset);
|
|
1165
|
+
const id = stringValue(record.assetKey ?? record.id) ?? typedAsset?.replace(/^assets\./, "");
|
|
1166
|
+
if (!id)
|
|
1167
|
+
continue;
|
|
1168
|
+
const nestedProvenance = objectValue(record.provenance);
|
|
1169
|
+
const sourcePath = stringValue(record.sourcePath ?? record.source ?? nestedProvenance?.builderOutput) ?? id;
|
|
1170
|
+
const license = stringValue(record.license ?? record.licenseNote ?? nestedProvenance?.license);
|
|
1171
|
+
const sourceUrl = stringValue(record.sourceUrl ?? record.publicUrl ?? record.officialPage ?? nestedProvenance?.sourceUrl);
|
|
1172
|
+
const sourceFamily = stringValue(record.sourceFamily ?? nestedProvenance?.sourceFamily ?? nestedProvenance?.sourcePack);
|
|
1173
|
+
const author = stringValue(record.author ?? nestedProvenance?.author);
|
|
1174
|
+
const attribution = stringValue(record.attribution ?? record.credit ?? nestedProvenance?.attribution);
|
|
1175
|
+
const evidence = [
|
|
1176
|
+
...stringArrayValue(record.evidence),
|
|
1177
|
+
...stringArrayValue(record.intendedRouteUsage),
|
|
1178
|
+
...stringArrayValue(nestedProvenance?.evidence)
|
|
1179
|
+
];
|
|
1180
|
+
byId.set(id, {
|
|
1181
|
+
sourcePath,
|
|
1182
|
+
...(sourceUrl ? { sourceUrl } : {}),
|
|
1183
|
+
...(license ? { license } : {}),
|
|
1184
|
+
...(author ? { author } : {}),
|
|
1185
|
+
...(sourceFamily ? { sourceFamily } : {}),
|
|
1186
|
+
...(attribution ? { attribution } : {}),
|
|
1187
|
+
...(evidence.length > 0 ? { evidence } : {}),
|
|
1188
|
+
checkedAt
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
return byId;
|
|
1192
|
+
}
|
|
1193
|
+
function arrayObjectValue(value) {
|
|
1194
|
+
if (!Array.isArray(value))
|
|
1195
|
+
return [];
|
|
1196
|
+
return value.map((entry) => objectValue(entry)).filter((entry) => Boolean(entry));
|
|
1197
|
+
}
|
|
1198
|
+
function resolveAssetProvenance(asset, externalProvenance) {
|
|
1199
|
+
return asset.provenance ?? externalProvenance.get(asset.id);
|
|
1200
|
+
}
|
|
1201
|
+
function hasUsableLicenseEvidence(provenance) {
|
|
1202
|
+
const license = provenance?.license?.trim();
|
|
1203
|
+
if (!license)
|
|
1204
|
+
return false;
|
|
1205
|
+
return !/(unverified|unknown|candidate|needs[-\s]?confirmation|todo|placeholder)/i.test(license);
|
|
1206
|
+
}
|
|
1207
|
+
function isPlaceholderAsset(asset, provenance) {
|
|
1208
|
+
const value = [
|
|
1209
|
+
asset.id,
|
|
1210
|
+
asset.source,
|
|
1211
|
+
asset.outputPath,
|
|
1212
|
+
asset.url,
|
|
1213
|
+
provenance?.sourcePath,
|
|
1214
|
+
provenance?.sourceUrl
|
|
1215
|
+
].filter(Boolean).join(" ");
|
|
1216
|
+
return /(^|[-_./\s])(placeholder|dummy|mock|todo|replace-me|sample-placeholder)([-_./\s]|$)/i.test(value);
|
|
1217
|
+
}
|
|
1218
|
+
function copyAssetDependencies(projectDir, sourcePath, outputDir, dependencies) {
|
|
1219
|
+
const sourceDir = dirname(sourcePath);
|
|
1220
|
+
for (const dependency of dependencies) {
|
|
1221
|
+
if (dependency.startsWith("data:"))
|
|
1222
|
+
continue;
|
|
1223
|
+
const sourceDependencyPath = resolve(sourceDir, dependency);
|
|
1224
|
+
const outputDependencyPath = resolve(projectDir, outputDir, dependency);
|
|
1225
|
+
mkdirSync(dirname(outputDependencyPath), { recursive: true });
|
|
1226
|
+
copyFileSync(sourceDependencyPath, outputDependencyPath);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function writeThumbnail(projectDir, outputDir, publicPath, id, bounds) {
|
|
1230
|
+
const fileName = `${id}.thumb.svg`;
|
|
1231
|
+
const outputPath = resolve(projectDir, outputDir, fileName);
|
|
1232
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
1233
|
+
const label = `${id}${bounds ? ` ${bounds.join("x")}` : ""}`;
|
|
1234
|
+
writeFileSync(outputPath, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 180"><rect width="320" height="180" fill="#101720"/><path d="M80 126 160 46l80 80-80 28z" fill="#77a7ff"/><text x="160" y="160" text-anchor="middle" font-family="Arial" font-size="18" fill="#f4f7fb">${escapeXml(label)}</text></svg>`);
|
|
1235
|
+
return `${publicPath}${fileName}`;
|
|
1236
|
+
}
|
|
1237
|
+
function inferAssetType(format) {
|
|
1238
|
+
if (["glb", "gltf"].includes(format))
|
|
1239
|
+
return "model";
|
|
1240
|
+
if (["png", "jpg", "jpeg", "webp", "ktx2"].includes(format))
|
|
1241
|
+
return "texture";
|
|
1242
|
+
if (["hdr", "exr"].includes(format))
|
|
1243
|
+
return "environment";
|
|
1244
|
+
if (["mp3", "wav", "ogg"].includes(format))
|
|
1245
|
+
return "audio";
|
|
1246
|
+
throw new Error(`Unsupported Aura3D asset format: ${format || "unknown"}. Suggested fix: use glb, gltf, png, jpg, webp, ktx2, hdr, exr, mp3, wav, or ogg.`);
|
|
1247
|
+
}
|
|
1248
|
+
function hashFile(path) {
|
|
1249
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
1250
|
+
}
|
|
1251
|
+
function sortManifest(manifest) {
|
|
1252
|
+
return {
|
|
1253
|
+
...manifest,
|
|
1254
|
+
assets: [...manifest.assets].sort((a, b) => a.id.localeCompare(b.id))
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
function normalizePublicPath(path) {
|
|
1258
|
+
const withStart = path.startsWith("/") || path.startsWith("http") ? path : `/${path}`;
|
|
1259
|
+
return withStart.endsWith("/") ? withStart : `${withStart}/`;
|
|
1260
|
+
}
|
|
1261
|
+
function normalizeRelativePath(path) {
|
|
1262
|
+
return path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
1263
|
+
}
|
|
1264
|
+
function sanitizeAssetId(value) {
|
|
1265
|
+
return value.replace(/[^a-zA-Z0-9_]+(.)/g, (_, char) => char.toUpperCase()).replace(/^[^a-zA-Z_]+/, "") || "asset";
|
|
1266
|
+
}
|
|
1267
|
+
function round(value) {
|
|
1268
|
+
return Math.round(value * 1000) / 1000;
|
|
1269
|
+
}
|
|
1270
|
+
function writeAgentFile(projectDir, path, contents) {
|
|
1271
|
+
const output = resolve(projectDir, path);
|
|
1272
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
1273
|
+
writeFileSync(output, contents);
|
|
1274
|
+
return output;
|
|
1275
|
+
}
|
|
1276
|
+
function genericAgentText(agent = "AI coding agent") {
|
|
1277
|
+
return `# Aura3D Instructions For ${agent}
|
|
1278
|
+
|
|
1279
|
+
Read ./llms.txt first, then ./docs/agents/README.md.
|
|
1280
|
+
|
|
1281
|
+
Use @aura3d/engine public imports only:
|
|
1282
|
+
- createAuraApp
|
|
1283
|
+
- scene
|
|
1284
|
+
- model
|
|
1285
|
+
- camera
|
|
1286
|
+
- lights
|
|
1287
|
+
- material
|
|
1288
|
+
- effects
|
|
1289
|
+
- timeline
|
|
1290
|
+
- interactions
|
|
1291
|
+
- defineAuraAssets
|
|
1292
|
+
|
|
1293
|
+
Do not invent asset paths or asset ids. Read ./src/aura-assets.ts after running:
|
|
1294
|
+
|
|
1295
|
+
npx @aura3d/cli@latest assets add ./assets/model.glb --name model
|
|
1296
|
+
|
|
1297
|
+
Use model(assets.model), not model("model").
|
|
1298
|
+
Run npm run build and the template route-health/screenshot tests before claiming the scene is done.
|
|
1299
|
+
`;
|
|
1300
|
+
}
|
|
1301
|
+
function escapeXml(value) {
|
|
1302
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
1303
|
+
}
|
|
1304
|
+
//# sourceMappingURL=index.js.map
|