@aura3d/engine 1.0.3 → 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/README.md +309 -7
- package/dist/animation/AnimationClipEvents.d.ts +57 -0
- package/dist/animation/AnimationClipEvents.d.ts.map +1 -0
- package/dist/animation/AnimationClipEvents.js +171 -0
- package/dist/animation/AnimationClipEvents.js.map +1 -0
- package/dist/animation/AnimationClipRegistry.d.ts +76 -0
- package/dist/animation/AnimationClipRegistry.d.ts.map +1 -0
- package/dist/animation/AnimationClipRegistry.js +130 -0
- package/dist/animation/AnimationClipRegistry.js.map +1 -0
- package/dist/animation/AnimationController.d.ts +168 -0
- package/dist/animation/AnimationController.d.ts.map +1 -0
- package/dist/animation/AnimationController.js +619 -0
- package/dist/animation/AnimationController.js.map +1 -0
- package/dist/animation/HumanoidRetargeting.d.ts +76 -0
- package/dist/animation/HumanoidRetargeting.d.ts.map +1 -0
- package/dist/animation/HumanoidRetargeting.js +331 -0
- package/dist/animation/HumanoidRetargeting.js.map +1 -0
- package/dist/animation/browser-index.d.ts +18 -0
- package/dist/animation/browser-index.d.ts.map +1 -1
- package/dist/animation/browser-index.js +13 -0
- package/dist/animation/browser-index.js.map +1 -1
- package/dist/animation/index.d.ts +16 -1
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/animation/index.js +11 -1
- package/dist/animation/index.js.map +1 -1
- package/dist/animation/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -1
- package/dist/animation/threejs-compatibility/AnimationDiagnostics.js +3 -5
- package/dist/animation/threejs-compatibility/AnimationDiagnostics.js.map +1 -1
- package/dist/assets/GLTFAnimationRuntime.js +1 -1
- package/dist/assets/GLTFLoader.js +1 -1
- package/dist/aura3d-cli/cli.js +194 -8
- package/dist/aura3d-cli/cli.js.map +1 -1
- package/dist/aura3d-cli/index.d.ts +280 -3
- package/dist/aura3d-cli/index.d.ts.map +1 -1
- package/dist/aura3d-cli/index.js +886 -4
- package/dist/aura3d-cli/index.js.map +1 -1
- package/dist/aura3d-cli/pull-bridge.d.ts +95 -0
- package/dist/aura3d-cli/pull-bridge.d.ts.map +1 -0
- package/dist/aura3d-cli/pull-bridge.js +247 -0
- package/dist/aura3d-cli/pull-bridge.js.map +1 -0
- package/dist/create-aura3d/index.d.ts +1 -1
- package/dist/create-aura3d/index.d.ts.map +1 -1
- package/dist/create-aura3d/index.js +9 -2
- package/dist/create-aura3d/index.js.map +1 -1
- package/dist/editor-runtime/ProjectSerializer.d.ts +74 -1
- package/dist/editor-runtime/ProjectSerializer.d.ts.map +1 -1
- package/dist/editor-runtime/ProjectSerializer.js +123 -6
- package/dist/editor-runtime/ProjectSerializer.js.map +1 -1
- package/dist/editor-runtime/TimelineModel.d.ts +18 -0
- package/dist/editor-runtime/TimelineModel.d.ts.map +1 -1
- package/dist/editor-runtime/TimelineModel.js +67 -3
- package/dist/editor-runtime/TimelineModel.js.map +1 -1
- package/dist/editor-runtime/TimelineRuntimeBridge.d.ts +98 -0
- package/dist/editor-runtime/TimelineRuntimeBridge.d.ts.map +1 -0
- package/dist/editor-runtime/TimelineRuntimeBridge.js +186 -0
- package/dist/editor-runtime/TimelineRuntimeBridge.js.map +1 -0
- package/dist/editor-runtime/index.d.ts +3 -1
- package/dist/editor-runtime/index.d.ts.map +1 -1
- package/dist/editor-runtime/index.js +1 -0
- package/dist/editor-runtime/index.js.map +1 -1
- package/dist/engine/agent-api/AnimationController.d.ts +607 -0
- package/dist/engine/agent-api/AnimationController.d.ts.map +1 -0
- package/dist/engine/agent-api/AnimationController.js +2192 -0
- package/dist/engine/agent-api/AnimationController.js.map +1 -0
- package/dist/engine/agent-api/AssetEvidence.d.ts +88 -0
- package/dist/engine/agent-api/AssetEvidence.d.ts.map +1 -0
- package/dist/engine/agent-api/AssetEvidence.js +157 -0
- package/dist/engine/agent-api/AssetEvidence.js.map +1 -0
- package/dist/engine/agent-api/AuraAppHandle.d.ts +55 -0
- package/dist/engine/agent-api/AuraAppHandle.d.ts.map +1 -0
- package/dist/engine/agent-api/AuraAppHandle.js +15 -0
- package/dist/engine/agent-api/AuraAppHandle.js.map +1 -0
- package/dist/engine/agent-api/AuraVoiceBridge.d.ts +96 -0
- package/dist/engine/agent-api/AuraVoiceBridge.d.ts.map +1 -0
- package/dist/engine/agent-api/AuraVoiceBridge.js +370 -0
- package/dist/engine/agent-api/AuraVoiceBridge.js.map +1 -0
- package/dist/engine/agent-api/CartoonDirector.d.ts +95 -0
- package/dist/engine/agent-api/CartoonDirector.d.ts.map +1 -0
- package/dist/engine/agent-api/CartoonDirector.js +342 -0
- package/dist/engine/agent-api/CartoonDirector.js.map +1 -0
- package/dist/engine/agent-api/CartoonPerformance.d.ts +149 -0
- package/dist/engine/agent-api/CartoonPerformance.d.ts.map +1 -0
- package/dist/engine/agent-api/CartoonPerformance.js +317 -0
- package/dist/engine/agent-api/CartoonPerformance.js.map +1 -0
- package/dist/engine/agent-api/CartoonRenderQueue.d.ts +132 -0
- package/dist/engine/agent-api/CartoonRenderQueue.d.ts.map +1 -0
- package/dist/engine/agent-api/CartoonRenderQueue.js +385 -0
- package/dist/engine/agent-api/CartoonRenderQueue.js.map +1 -0
- package/dist/engine/agent-api/CharacterAssembly.d.ts +126 -0
- package/dist/engine/agent-api/CharacterAssembly.d.ts.map +1 -0
- package/dist/engine/agent-api/CharacterAssembly.js +280 -0
- package/dist/engine/agent-api/CharacterAssembly.js.map +1 -0
- package/dist/engine/agent-api/DialoguePerformance.d.ts +150 -0
- package/dist/engine/agent-api/DialoguePerformance.d.ts.map +1 -0
- package/dist/engine/agent-api/DialoguePerformance.js +335 -0
- package/dist/engine/agent-api/DialoguePerformance.js.map +1 -0
- package/dist/engine/agent-api/FrameLoop.d.ts +70 -0
- package/dist/engine/agent-api/FrameLoop.d.ts.map +1 -0
- package/dist/engine/agent-api/FrameLoop.js +165 -0
- package/dist/engine/agent-api/FrameLoop.js.map +1 -0
- package/dist/engine/agent-api/GameAssetValidation.d.ts +279 -0
- package/dist/engine/agent-api/GameAssetValidation.d.ts.map +1 -0
- package/dist/engine/agent-api/GameAssetValidation.js +719 -0
- package/dist/engine/agent-api/GameAssetValidation.js.map +1 -0
- package/dist/engine/agent-api/GameEvidence.d.ts +148 -0
- package/dist/engine/agent-api/GameEvidence.d.ts.map +1 -0
- package/dist/engine/agent-api/GameEvidence.js +269 -0
- package/dist/engine/agent-api/GameEvidence.js.map +1 -0
- package/dist/engine/agent-api/GameRuntime.d.ts +931 -0
- package/dist/engine/agent-api/GameRuntime.d.ts.map +1 -0
- package/dist/engine/agent-api/GameRuntime.js +2229 -0
- package/dist/engine/agent-api/GameRuntime.js.map +1 -0
- package/dist/engine/agent-api/GameSceneBridge.d.ts +54 -0
- package/dist/engine/agent-api/GameSceneBridge.d.ts.map +1 -0
- package/dist/engine/agent-api/GameSceneBridge.js +110 -0
- package/dist/engine/agent-api/GameSceneBridge.js.map +1 -0
- package/dist/engine/agent-api/PromptAnimationContract.d.ts +278 -0
- package/dist/engine/agent-api/PromptAnimationContract.d.ts.map +1 -0
- package/dist/engine/agent-api/PromptAnimationContract.js +238 -0
- package/dist/engine/agent-api/PromptAnimationContract.js.map +1 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.d.ts +183 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.d.ts.map +1 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.js +454 -0
- package/dist/engine/agent-api/PromptAnimationEvidence.js.map +1 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.d.ts +100 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.d.ts.map +1 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.js +36 -0
- package/dist/engine/agent-api/RuntimeNodeHandle.js.map +1 -0
- package/dist/engine/agent-api/ShotTimeline.d.ts +179 -0
- package/dist/engine/agent-api/ShotTimeline.d.ts.map +1 -0
- package/dist/engine/agent-api/ShotTimeline.js +264 -0
- package/dist/engine/agent-api/ShotTimeline.js.map +1 -0
- package/dist/engine/agent-api/VisemeController.d.ts +89 -0
- package/dist/engine/agent-api/VisemeController.d.ts.map +1 -0
- package/dist/engine/agent-api/VisemeController.js +207 -0
- package/dist/engine/agent-api/VisemeController.js.map +1 -0
- package/dist/engine/agent-api/game-kits/fighting.d.ts +123 -0
- package/dist/engine/agent-api/game-kits/fighting.d.ts.map +1 -0
- package/dist/engine/agent-api/game-kits/fighting.js +483 -0
- package/dist/engine/agent-api/game-kits/fighting.js.map +1 -0
- package/dist/engine/agent-api/game-kits/index.d.ts +15 -0
- package/dist/engine/agent-api/game-kits/index.d.ts.map +1 -0
- package/dist/engine/agent-api/game-kits/index.js +6 -0
- package/dist/engine/agent-api/game-kits/index.js.map +1 -0
- package/dist/engine/agent-api/humanoid-walk-runtime.d.ts +1 -0
- package/dist/engine/agent-api/humanoid-walk-runtime.d.ts.map +1 -1
- package/dist/engine/agent-api/index.d.ts +487 -1
- package/dist/engine/agent-api/index.d.ts.map +1 -1
- package/dist/engine/agent-api/index.js +740 -6
- package/dist/engine/agent-api/index.js.map +1 -1
- package/dist/engine/agent-api/product-viewer-runtime.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/physics/CollisionVolumes.d.ts +57 -0
- package/dist/physics/CollisionVolumes.d.ts.map +1 -0
- package/dist/physics/CollisionVolumes.js +159 -0
- package/dist/physics/CollisionVolumes.js.map +1 -0
- package/dist/physics/HitboxWorld.d.ts +229 -0
- package/dist/physics/HitboxWorld.d.ts.map +1 -0
- package/dist/physics/HitboxWorld.js +640 -0
- package/dist/physics/HitboxWorld.js.map +1 -0
- package/dist/physics/KinematicBody.d.ts +157 -0
- package/dist/physics/KinematicBody.d.ts.map +1 -0
- package/dist/physics/KinematicBody.js +405 -0
- package/dist/physics/KinematicBody.js.map +1 -0
- package/dist/physics/KinematicWorld.d.ts +58 -0
- package/dist/physics/KinematicWorld.d.ts.map +1 -0
- package/dist/physics/KinematicWorld.js +246 -0
- package/dist/physics/KinematicWorld.js.map +1 -0
- package/dist/physics/index.d.ts +4 -0
- package/dist/physics/index.d.ts.map +1 -1
- package/dist/physics/index.js +4 -0
- package/dist/physics/index.js.map +1 -1
- package/dist/rendering/ForwardPass.js +2 -2
- package/dist/rendering/ShaderLibrary.js +2 -2
- package/dist/rendering/SkinnedLitMaterial.js +3 -3
- package/dist/rendering/SkinnedUnlitMaterial.js +3 -3
- package/dist/scene/Renderable.js +2 -2
- package/dist/scripting/VisualGraph.d.ts +2 -1
- package/dist/scripting/VisualGraph.d.ts.map +1 -1
- package/dist/scripting/VisualGraph.js +118 -1
- package/dist/scripting/VisualGraph.js.map +1 -1
- package/dist/scripting/VisualGraphContext.d.ts +123 -0
- package/dist/scripting/VisualGraphContext.d.ts.map +1 -0
- package/dist/scripting/VisualGraphContext.js +2 -0
- package/dist/scripting/VisualGraphContext.js.map +1 -0
- package/dist/scripting/VisualGraphExecutor.d.ts +6 -1
- package/dist/scripting/VisualGraphExecutor.d.ts.map +1 -1
- package/dist/scripting/VisualGraphExecutor.js +364 -7
- package/dist/scripting/VisualGraphExecutor.js.map +1 -1
- package/dist/scripting/VisualNodeCatalog.d.ts +1 -1
- package/dist/scripting/VisualNodeCatalog.d.ts.map +1 -1
- package/dist/scripting/VisualNodeCatalog.js +61 -1
- package/dist/scripting/VisualNodeCatalog.js.map +1 -1
- package/dist/scripting/index.d.ts +1 -0
- package/dist/scripting/index.d.ts.map +1 -1
- package/dist/scripting/index.js.map +1 -1
- package/package.json +192 -118
package/dist/aura3d-cli/index.js
CHANGED
|
@@ -38,10 +38,19 @@ export function addAsset(options) {
|
|
|
38
38
|
hash: `sha256-${hash}`,
|
|
39
39
|
sizeBytes: statSync(sourcePath).size,
|
|
40
40
|
bounds: inspection.bounds,
|
|
41
|
+
boundsMetadata: inspection.boundsMetadata,
|
|
41
42
|
materials: inspection.materials,
|
|
43
|
+
materialMetadata: inspection.materialMetadata,
|
|
42
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),
|
|
43
50
|
textures: inspection.textures,
|
|
44
51
|
dependencies: inspection.dependencies,
|
|
52
|
+
orientation: inspection.orientation,
|
|
53
|
+
nodeNames: inspection.nodeNames,
|
|
45
54
|
thumbnailUrl,
|
|
46
55
|
warnings: createAssetWarnings(sourcePath, inspection)
|
|
47
56
|
};
|
|
@@ -91,14 +100,55 @@ export function scanAssets(options) {
|
|
|
91
100
|
messages: ["No supported assets found."]
|
|
92
101
|
};
|
|
93
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
|
+
}
|
|
94
136
|
export function validateAssets(options = {}) {
|
|
95
137
|
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
96
138
|
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
97
139
|
const manifestMissing = !existsSync(manifestPath);
|
|
98
|
-
const
|
|
140
|
+
const sourceManifest = readAssetManifest(projectDir);
|
|
141
|
+
const manifest = filterAssetManifest(sourceManifest, options.assetIds);
|
|
142
|
+
const externalProvenance = readExternalProvenance(projectDir, options.provenanceFile);
|
|
99
143
|
const failures = manifestMissing
|
|
100
144
|
? [`Missing ${DEFAULT_AURA_ASSET_MANIFEST}. Suggested fix: run aura3d assets add ./asset.glb --name product or aura3d assets scan ./assets.`]
|
|
101
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
|
+
}
|
|
102
152
|
const warnings = [];
|
|
103
153
|
for (const asset of manifest.assets) {
|
|
104
154
|
const outputPath = resolve(projectDir, asset.outputPath);
|
|
@@ -109,6 +159,13 @@ export function validateAssets(options = {}) {
|
|
|
109
159
|
const actualHash = `sha256-${hashFile(outputPath)}`;
|
|
110
160
|
if (actualHash !== asset.hash)
|
|
111
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
|
+
}
|
|
112
169
|
warnings.push(...asset.warnings.map((warning) => `${asset.id}: ${warning}`));
|
|
113
170
|
if (asset.format === "gltf") {
|
|
114
171
|
for (const dependency of asset.dependencies ?? asset.textures) {
|
|
@@ -145,6 +202,22 @@ export function writeTypedAssets(projectDir, manifest = readAssetManifest(projec
|
|
|
145
202
|
const metadata = {
|
|
146
203
|
materials: asset.materials,
|
|
147
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 ?? [],
|
|
148
221
|
textures: asset.textures,
|
|
149
222
|
dependencies: asset.dependencies ?? [],
|
|
150
223
|
thumbnailUrl: asset.thumbnailUrl
|
|
@@ -228,6 +301,97 @@ export function checkDeploy(options = {}) {
|
|
|
228
301
|
messages: failures.length === 0 ? ["Deploy check passed."] : failures
|
|
229
302
|
};
|
|
230
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
|
+
}
|
|
231
395
|
export function initAgentFiles(options) {
|
|
232
396
|
const projectDir = resolve(options.projectDir ?? process.cwd());
|
|
233
397
|
const targets = options.agent === "all" ? ["generic", "claude", "cursor", "copilot"] : [options.agent];
|
|
@@ -244,6 +408,339 @@ export function initAgentFiles(options) {
|
|
|
244
408
|
}
|
|
245
409
|
return written;
|
|
246
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
|
+
}
|
|
247
744
|
export function readAssetManifest(projectDir) {
|
|
248
745
|
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
249
746
|
if (!existsSync(manifestPath)) {
|
|
@@ -270,8 +767,15 @@ function inspectAssetFile(path, format) {
|
|
|
270
767
|
return inspectGlb(readFileSync(path), dirname(path));
|
|
271
768
|
return {
|
|
272
769
|
materials: [],
|
|
770
|
+
materialMetadata: [],
|
|
273
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."),
|
|
274
776
|
textures: [],
|
|
777
|
+
orientation: unknownOrientationInspection(),
|
|
778
|
+
nodeNames: [],
|
|
275
779
|
dependencies: [],
|
|
276
780
|
bounds: undefined
|
|
277
781
|
};
|
|
@@ -300,18 +804,226 @@ function inspectGltf(json, baseDir) {
|
|
|
300
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.`);
|
|
301
805
|
}
|
|
302
806
|
}
|
|
807
|
+
const boundsMetadata = extractBoundsDetails(json);
|
|
303
808
|
return {
|
|
304
|
-
bounds:
|
|
809
|
+
bounds: boundsMetadata?.size,
|
|
810
|
+
boundsMetadata,
|
|
305
811
|
materials: (json.materials ?? []).map((material, index) => material.name ?? `material-${index}`),
|
|
812
|
+
materialMetadata: inspectGltfMaterials(json),
|
|
306
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),
|
|
307
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}`),
|
|
308
822
|
dependencies
|
|
309
823
|
};
|
|
310
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
|
+
}
|
|
311
1014
|
function isExternalUri(uri) {
|
|
312
1015
|
return typeof uri === "string" && uri.length > 0 && !uri.startsWith("data:");
|
|
313
1016
|
}
|
|
314
|
-
function
|
|
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) {
|
|
315
1027
|
let min;
|
|
316
1028
|
let max;
|
|
317
1029
|
for (const accessor of json.accessors ?? []) {
|
|
@@ -320,7 +1032,89 @@ function extractBounds(json) {
|
|
|
320
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]];
|
|
321
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]];
|
|
322
1034
|
}
|
|
323
|
-
|
|
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;
|
|
324
1118
|
}
|
|
325
1119
|
function createAssetWarnings(path, inspection) {
|
|
326
1120
|
const warnings = [];
|
|
@@ -331,8 +1125,96 @@ function createAssetWarnings(path, inspection) {
|
|
|
331
1125
|
warnings.push("bounds could not be extracted");
|
|
332
1126
|
if (inspection.textures.length === 0 && ["glb", "gltf"].includes(extname(path).slice(1).toLowerCase()))
|
|
333
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");
|
|
334
1132
|
return warnings;
|
|
335
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
|
+
}
|
|
336
1218
|
function copyAssetDependencies(projectDir, sourcePath, outputDir, dependencies) {
|
|
337
1219
|
const sourceDir = dirname(sourcePath);
|
|
338
1220
|
for (const dependency of dependencies) {
|