@aura3d/engine 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +338 -14
- 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/AnimationStateGraph.d.ts +2 -0
- package/dist/animation/AnimationStateGraph.d.ts.map +1 -0
- package/dist/animation/AnimationStateGraph.js +2 -0
- package/dist/animation/AnimationStateGraph.js.map +1 -0
- package/dist/animation/AnimationStateMachine.d.ts +16 -0
- package/dist/animation/AnimationStateMachine.d.ts.map +1 -1
- package/dist/animation/AnimationStateMachine.js +69 -7
- package/dist/animation/AnimationStateMachine.js.map +1 -1
- 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 +17 -1
- package/dist/animation/index.d.ts.map +1 -1
- package/dist/animation/index.js +12 -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 +225 -8
- package/dist/aura3d-cli/cli.js.map +1 -1
- package/dist/aura3d-cli/index.d.ts +283 -3
- package/dist/aura3d-cli/index.d.ts.map +1 -1
- package/dist/aura3d-cli/index.js +1028 -4
- package/dist/aura3d-cli/index.js.map +1 -1
- package/dist/aura3d-cli/pull-bridge.d.ts +108 -0
- package/dist/aura3d-cli/pull-bridge.d.ts.map +1 -0
- package/dist/aura3d-cli/pull-bridge.js +333 -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/GameAppRuntime.d.ts +62 -0
- package/dist/engine/agent-api/GameAppRuntime.d.ts.map +1 -0
- package/dist/engine/agent-api/GameAppRuntime.js +189 -0
- package/dist/engine/agent-api/GameAppRuntime.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 +495 -1
- package/dist/engine/agent-api/index.d.ts.map +1 -1
- package/dist/engine/agent-api/index.js +752 -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 +250 -0
- package/dist/physics/HitboxWorld.d.ts.map +1 -0
- package/dist/physics/HitboxWorld.js +771 -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 +203 -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,481 @@ 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 gameProfile = profile === "game" ? options.gameProfile : undefined;
|
|
414
|
+
const sourceManifest = readAssetManifest(projectDir);
|
|
415
|
+
const manifest = filterAssetManifest(sourceManifest, options.assetIds);
|
|
416
|
+
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
417
|
+
const evidencePath = options.output ? resolve(projectDir, options.output) : undefined;
|
|
418
|
+
const validation = validateAssets({
|
|
419
|
+
projectDir,
|
|
420
|
+
noPlaceholders: options.noPlaceholders,
|
|
421
|
+
requireLicense: options.requireLicense,
|
|
422
|
+
provenanceFile: options.provenanceFile,
|
|
423
|
+
assetIds: options.assetIds
|
|
424
|
+
});
|
|
425
|
+
const externalProvenance = readExternalProvenance(projectDir, options.provenanceFile);
|
|
426
|
+
const failures = [...validation.failures];
|
|
427
|
+
const warnings = [...validation.warnings];
|
|
428
|
+
const modelAssets = manifest.assets.filter((asset) => asset.type === "model");
|
|
429
|
+
const animatedModels = modelAssets.filter((asset) => asset.animations.length > 0);
|
|
430
|
+
const animationClips = manifest.assets.reduce((total, asset) => total + asset.animations.length, 0);
|
|
431
|
+
const humanoidModels = modelAssets.filter((asset) => asset.humanoid?.humanoid).length;
|
|
432
|
+
const artifacts = createReadinessArtifacts(projectDir, manifest, evidencePath);
|
|
433
|
+
const assets = manifest.assets.map((asset) => {
|
|
434
|
+
const provenance = resolveAssetProvenance(asset, externalProvenance);
|
|
435
|
+
const placeholderFree = !isPlaceholderAsset(asset, provenance);
|
|
436
|
+
const licenseVerified = hasUsableLicenseEvidence(provenance);
|
|
437
|
+
const assetWarnings = [...asset.warnings];
|
|
438
|
+
const readinessIssues = createAssetReadinessIssues(profile, asset);
|
|
439
|
+
const profileIssues = gameProfile === "fighting-character"
|
|
440
|
+
? createFightingCharacterReadinessIssues(asset, provenance, licenseVerified)
|
|
441
|
+
: { failures: [], warnings: [] };
|
|
442
|
+
if (asset.type === "model" && !asset.bounds)
|
|
443
|
+
assetWarnings.push("Missing bounds; camera framing, collision proxies, and thumbnail composition will be weaker.");
|
|
444
|
+
if (asset.type === "model" && asset.materials.length === 0)
|
|
445
|
+
assetWarnings.push("No material names detected; authored visual diagnostics will be limited.");
|
|
446
|
+
if (asset.type === "model" && asset.sizeBytes > 50 * 1024 * 1024)
|
|
447
|
+
assetWarnings.push("Large model over 50MB; consider mesh/texture optimization before browser deployment.");
|
|
448
|
+
if (asset.type === "model" && asset.animations.length > 0 && asset.humanoid?.status === "unknown")
|
|
449
|
+
assetWarnings.push("Animated model has unknown humanoid status; inspect with --humanoid before using it as an acted character.");
|
|
450
|
+
assetWarnings.push(...readinessIssues.warnings);
|
|
451
|
+
assetWarnings.push(...profileIssues.warnings);
|
|
452
|
+
pushUnique(failures, [...readinessIssues.failures, ...profileIssues.failures]);
|
|
453
|
+
pushUnique(warnings, [...readinessIssues.warnings, ...profileIssues.warnings]);
|
|
454
|
+
const gameReady = asset.type === "model" && Boolean(asset.bounds) && asset.materials.length > 0 && asset.sizeBytes <= 50 * 1024 * 1024 && readinessIssues.failures.length === 0 && profileIssues.failures.length === 0;
|
|
455
|
+
const cartoonReady = asset.type === "model"
|
|
456
|
+
? Boolean(asset.bounds) && (asset.animations.length > 0 || /prop|set|stage|background|environment/i.test(asset.id))
|
|
457
|
+
: asset.type === "audio" || asset.type === "texture";
|
|
458
|
+
const artifactPaths = artifacts.assetFiles.find((entry) => entry.id === asset.id) ?? createReadinessAssetArtifacts(projectDir, manifest, asset);
|
|
459
|
+
return {
|
|
460
|
+
id: asset.id,
|
|
461
|
+
type: asset.type,
|
|
462
|
+
format: asset.format,
|
|
463
|
+
source: asset.source,
|
|
464
|
+
outputPath: asset.outputPath,
|
|
465
|
+
url: asset.url,
|
|
466
|
+
sizeBytes: asset.sizeBytes,
|
|
467
|
+
bounds: asset.bounds,
|
|
468
|
+
boundsMetadata: asset.boundsMetadata,
|
|
469
|
+
animations: asset.animations,
|
|
470
|
+
animation: createReadinessAnimationMetadata(asset.animations),
|
|
471
|
+
animationMetadata: asset.animationMetadata,
|
|
472
|
+
humanoid: asset.humanoid,
|
|
473
|
+
skeleton: asset.skeleton,
|
|
474
|
+
morphTargets: asset.morphTargets,
|
|
475
|
+
provenance,
|
|
476
|
+
placeholderFree,
|
|
477
|
+
licenseVerified,
|
|
478
|
+
materials: asset.materials,
|
|
479
|
+
materialMetadata: asset.materialMetadata,
|
|
480
|
+
textures: asset.textures,
|
|
481
|
+
orientation: asset.orientation,
|
|
482
|
+
nodeNames: asset.nodeNames,
|
|
483
|
+
artifactPaths,
|
|
484
|
+
gameReady,
|
|
485
|
+
cartoonReady,
|
|
486
|
+
warnings: assetWarnings
|
|
487
|
+
};
|
|
488
|
+
});
|
|
489
|
+
if (profile === "game") {
|
|
490
|
+
if (modelAssets.length === 0)
|
|
491
|
+
failures.push("Game readiness requires at least one typed model asset. Add a GLB/GLTF with aura3d assets add ./fighter.glb --name fighter.");
|
|
492
|
+
if (animatedModels.length === 0)
|
|
493
|
+
warnings.push("No animated model clips detected. Static scenes can ship, but playable character showcases should include idle/walk/attack/hurt clips.");
|
|
494
|
+
if (animatedModels.length > 0 && humanoidModels === 0)
|
|
495
|
+
warnings.push("No humanoid model metadata detected. Character-heavy game routes should confirm humanoid status with assets inspect --humanoid and typed asset metadata.");
|
|
496
|
+
for (const asset of assets) {
|
|
497
|
+
if (asset.type !== "model")
|
|
498
|
+
continue;
|
|
499
|
+
if (!asset.gameReady)
|
|
500
|
+
warnings.push(`${asset.id}: not game-ready yet; expected bounds, named materials, and browser-sized payload.`);
|
|
501
|
+
}
|
|
502
|
+
if (gameProfile === "fighting-character") {
|
|
503
|
+
if (modelAssets.length < 1)
|
|
504
|
+
failures.push("fighting-character profile requires at least one typed fighter model.");
|
|
505
|
+
if (animatedModels.length < modelAssets.length)
|
|
506
|
+
failures.push("fighting-character profile requires every selected model asset to include embedded animation clips.");
|
|
507
|
+
if (humanoidModels < modelAssets.length)
|
|
508
|
+
failures.push("fighting-character profile requires every selected model asset to include humanoid/skeleton metadata.");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
if (modelAssets.length === 0)
|
|
513
|
+
failures.push("Cartoon readiness requires at least one typed model/set/prop GLB or GLTF.");
|
|
514
|
+
if (animatedModels.length === 0)
|
|
515
|
+
warnings.push("No animated character clips detected. Prompt-to-episode output can use transform animation, but character acting needs skeletal or pose clips.");
|
|
516
|
+
if (animatedModels.length > 0 && humanoidModels === 0)
|
|
517
|
+
warnings.push("No humanoid model metadata detected. Acting-heavy cartoon routes should confirm character rigs with assets inspect --humanoid.");
|
|
518
|
+
const audioAssets = manifest.assets.filter((asset) => asset.type === "audio");
|
|
519
|
+
if (audioAssets.length === 0)
|
|
520
|
+
warnings.push("No audio assets detected. AuraVoice bridge can still reference external narration manifests, but local episode proof is stronger with audio registered.");
|
|
521
|
+
}
|
|
522
|
+
const ok = failures.length === 0;
|
|
523
|
+
const baseMessage = ok
|
|
524
|
+
? `${profile === "game" ? "Game" : "Cartoon"} asset readiness report completed.`
|
|
525
|
+
: failures;
|
|
526
|
+
const messages = [
|
|
527
|
+
...(Array.isArray(baseMessage) ? baseMessage : [baseMessage]),
|
|
528
|
+
...(evidencePath ? [`Wrote asset readiness evidence: ${normalizeRelativePath(relative(projectDir, evidencePath))}`] : [])
|
|
529
|
+
];
|
|
530
|
+
const report = {
|
|
531
|
+
schema: "aura3d.asset-readiness/1.0",
|
|
532
|
+
profile,
|
|
533
|
+
...(gameProfile ? { gameProfile } : {}),
|
|
534
|
+
ok,
|
|
535
|
+
status: ok ? "passed" : "failed",
|
|
536
|
+
validator: createReadinessValidatorEvidence(profile),
|
|
537
|
+
checkedAt: new Date().toISOString(),
|
|
538
|
+
manifestPath,
|
|
539
|
+
artifacts,
|
|
540
|
+
contracts: createReadinessValidationContracts(profile),
|
|
541
|
+
summary: {
|
|
542
|
+
totalAssets: manifest.assets.length,
|
|
543
|
+
modelAssets: modelAssets.length,
|
|
544
|
+
animatedModels: animatedModels.length,
|
|
545
|
+
textureAssets: manifest.assets.filter((asset) => asset.type === "texture").length,
|
|
546
|
+
audioAssets: manifest.assets.filter((asset) => asset.type === "audio").length,
|
|
547
|
+
environmentAssets: manifest.assets.filter((asset) => asset.type === "environment").length,
|
|
548
|
+
animationClips,
|
|
549
|
+
humanoidModels
|
|
550
|
+
},
|
|
551
|
+
assets,
|
|
552
|
+
failures,
|
|
553
|
+
warnings,
|
|
554
|
+
messages
|
|
555
|
+
};
|
|
556
|
+
if (evidencePath) {
|
|
557
|
+
mkdirSync(dirname(evidencePath), { recursive: true });
|
|
558
|
+
writeFileSync(evidencePath, `${JSON.stringify(report, null, 2)}\n`);
|
|
559
|
+
}
|
|
560
|
+
return report;
|
|
561
|
+
}
|
|
562
|
+
function filterAssetManifest(manifest, assetIds) {
|
|
563
|
+
const normalized = normalizeAssetIdFilter(assetIds);
|
|
564
|
+
if (normalized.length === 0)
|
|
565
|
+
return manifest;
|
|
566
|
+
const allowed = new Set(normalized);
|
|
567
|
+
return {
|
|
568
|
+
...manifest,
|
|
569
|
+
assets: manifest.assets.filter((asset) => allowed.has(asset.id))
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function findMissingAssetIds(manifest, assetIds) {
|
|
573
|
+
const normalized = normalizeAssetIdFilter(assetIds);
|
|
574
|
+
if (normalized.length === 0)
|
|
575
|
+
return [];
|
|
576
|
+
const existing = new Set(manifest.assets.map((asset) => asset.id));
|
|
577
|
+
return normalized.filter((id) => !existing.has(id));
|
|
578
|
+
}
|
|
579
|
+
function normalizeAssetIdFilter(assetIds) {
|
|
580
|
+
return [...new Set((assetIds ?? []).map((id) => id.trim()).filter(Boolean))];
|
|
581
|
+
}
|
|
582
|
+
function createReadinessValidatorEvidence(profile) {
|
|
583
|
+
return profile === "game"
|
|
584
|
+
? {
|
|
585
|
+
id: "aura-clash-game-assets",
|
|
586
|
+
command: "assets validate-game",
|
|
587
|
+
label: "AuraClash game asset validator"
|
|
588
|
+
}
|
|
589
|
+
: {
|
|
590
|
+
id: "aura-voice-cartoon-assets",
|
|
591
|
+
command: "assets validate-cartoon",
|
|
592
|
+
label: "AuraVoice cartoon asset validator"
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function createReadinessValidationContracts(profile) {
|
|
596
|
+
if (profile === "game") {
|
|
597
|
+
return [
|
|
598
|
+
{
|
|
599
|
+
id: "quaternius-game-ready-fighter-validation-contract",
|
|
600
|
+
label: "Quaternius-derived game-ready fighter validation contract",
|
|
601
|
+
profile: "game",
|
|
602
|
+
sourceFamily: "Quaternius",
|
|
603
|
+
intendedUse: "fighter",
|
|
604
|
+
sourceOnly: true,
|
|
605
|
+
requiredChecks: [
|
|
606
|
+
"typed Aura model asset entry generated by assets add",
|
|
607
|
+
"Quaternius provenance or source-family metadata",
|
|
608
|
+
"GLB/GLTF model with browser-sized payload",
|
|
609
|
+
"bounds with grounded pivot and fighter-scale dimensions",
|
|
610
|
+
"forward-facing +z or z orientation before runtime mirroring",
|
|
611
|
+
"humanoid skeleton metadata suitable for retarget diagnostics",
|
|
612
|
+
"readable visible materials and texture budget",
|
|
613
|
+
"thumbnail or first-frame artifact path",
|
|
614
|
+
"non-empty named fighting animation clips",
|
|
615
|
+
"no floating hair-only assembly without a body/head anchor"
|
|
616
|
+
],
|
|
617
|
+
requiredAnimationClips: ["idle", "walk", "lightPunch"],
|
|
618
|
+
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."
|
|
619
|
+
}
|
|
620
|
+
];
|
|
621
|
+
}
|
|
622
|
+
return [
|
|
623
|
+
{
|
|
624
|
+
id: "auravoice-cartoon-character-asset-validation-contract",
|
|
625
|
+
label: "AuraVoice cartoon asset validation contract",
|
|
626
|
+
profile: "cartoon",
|
|
627
|
+
sourceFamily: "AuraVoice",
|
|
628
|
+
intendedUse: "cartoon-character",
|
|
629
|
+
sourceOnly: true,
|
|
630
|
+
requiredChecks: [
|
|
631
|
+
"typed Aura model, texture, audio, or environment asset entry",
|
|
632
|
+
"bounds for model/set composition",
|
|
633
|
+
"animation or transform-ready character metadata",
|
|
634
|
+
"audio or external AuraVoice manifest references for stronger episode proof"
|
|
635
|
+
],
|
|
636
|
+
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."
|
|
637
|
+
}
|
|
638
|
+
];
|
|
639
|
+
}
|
|
640
|
+
function createReadinessArtifacts(projectDir, manifest, evidencePath) {
|
|
641
|
+
const artifacts = {
|
|
642
|
+
manifestPath: resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST),
|
|
643
|
+
typedAssetsPath: resolve(projectDir, manifest.typegen),
|
|
644
|
+
outputDir: resolve(projectDir, manifest.outputDir),
|
|
645
|
+
assetBasePath: manifest.assetBasePath,
|
|
646
|
+
assetFiles: manifest.assets.map((asset) => createReadinessAssetArtifacts(projectDir, manifest, asset))
|
|
647
|
+
};
|
|
648
|
+
if (evidencePath)
|
|
649
|
+
artifacts.evidencePath = evidencePath;
|
|
650
|
+
return artifacts;
|
|
651
|
+
}
|
|
652
|
+
function createReadinessAssetArtifacts(projectDir, manifest, asset) {
|
|
653
|
+
const artifact = {
|
|
654
|
+
id: asset.id,
|
|
655
|
+
sourcePath: resolve(projectDir, asset.source),
|
|
656
|
+
outputPath: resolve(projectDir, asset.outputPath),
|
|
657
|
+
publicUrl: asset.url,
|
|
658
|
+
dependencyPaths: (asset.dependencies ?? []).map((dependency) => resolve(dirname(resolve(projectDir, asset.outputPath)), dependency))
|
|
659
|
+
};
|
|
660
|
+
if (asset.thumbnailUrl) {
|
|
661
|
+
artifact.thumbnailUrl = asset.thumbnailUrl;
|
|
662
|
+
const thumbnailPath = resolvePublicArtifactPath(projectDir, manifest, asset.thumbnailUrl);
|
|
663
|
+
if (thumbnailPath)
|
|
664
|
+
artifact.thumbnailPath = thumbnailPath;
|
|
665
|
+
}
|
|
666
|
+
return artifact;
|
|
667
|
+
}
|
|
668
|
+
function createReadinessAnimationMetadata(animations) {
|
|
669
|
+
return {
|
|
670
|
+
clipCount: animations.length,
|
|
671
|
+
clips: animations.map((name, index) => ({ index, name }))
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function createAssetReadinessIssues(profile, asset) {
|
|
675
|
+
if (asset.type !== "model")
|
|
676
|
+
return { failures: [], warnings: [] };
|
|
677
|
+
const failures = [];
|
|
678
|
+
const warnings = [];
|
|
679
|
+
const prefix = `${asset.id}:`;
|
|
680
|
+
const characterLike = isCharacterLikeAsset(asset);
|
|
681
|
+
if (profile === "game" && characterLike) {
|
|
682
|
+
const missing = missingRequiredGameClips(asset.animations);
|
|
683
|
+
if (missing.length > 0) {
|
|
684
|
+
failures.push(`${prefix} missing required game animation clip${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}.`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const emptyClips = (asset.animationMetadata?.clips ?? []).filter((clip) => clip.channelCount === 0 || clip.samplerCount === 0);
|
|
688
|
+
for (const clip of emptyClips) {
|
|
689
|
+
failures.push(`${prefix} animation clip "${clip.name}" is empty; expected at least one channel and sampler.`);
|
|
690
|
+
}
|
|
691
|
+
const bounds = asset.boundsMetadata;
|
|
692
|
+
if (bounds && bounds.maxDimension > 50) {
|
|
693
|
+
failures.push(`${prefix} oversized bounds detected; largest dimension is ${bounds.maxDimension}m, expected at most 50m for browser game assets.`);
|
|
694
|
+
}
|
|
695
|
+
else if (bounds && characterLike && bounds.maxDimension > 4) {
|
|
696
|
+
warnings.push(`${prefix} character-sized model is unusually large (${bounds.maxDimension}m); confirm scale before using it in gameplay.`);
|
|
697
|
+
}
|
|
698
|
+
if (bounds && characterLike && !bounds.grounded) {
|
|
699
|
+
warnings.push(`${prefix} bounds are not grounded at the pivot; min.y is ${bounds.min[1]}m.`);
|
|
700
|
+
}
|
|
701
|
+
const orientation = asset.orientation;
|
|
702
|
+
if (profile === "game" && characterLike && orientation?.forwardAxis && !["+z", "z"].includes(orientation.forwardAxis.toLowerCase())) {
|
|
703
|
+
failures.push(`${prefix} wrong facing direction "${orientation.forwardAxis}"; fighting-game characters are expected to face +z before runtime mirroring.`);
|
|
704
|
+
}
|
|
705
|
+
const invisibleMaterials = (asset.materialMetadata ?? []).filter((material) => !material.visible || !material.readable);
|
|
706
|
+
for (const material of invisibleMaterials) {
|
|
707
|
+
failures.push(`${prefix} invisible or unreadable material "${material.name}" detected${material.reasons.length ? ` (${material.reasons.join("; ")})` : ""}.`);
|
|
708
|
+
}
|
|
709
|
+
if (profile === "game" && hasFloatingHairRisk(asset)) {
|
|
710
|
+
failures.push(`${prefix} floating hair risk detected; hair-only geometry must be assembled onto a body/head with assets assemble-character before game validation.`);
|
|
711
|
+
}
|
|
712
|
+
return { failures, warnings };
|
|
713
|
+
}
|
|
714
|
+
function createFightingCharacterReadinessIssues(asset, provenance, licenseVerified) {
|
|
715
|
+
const failures = [];
|
|
716
|
+
const warnings = [];
|
|
717
|
+
const prefix = `${asset.id}:`;
|
|
718
|
+
if (asset.type !== "model") {
|
|
719
|
+
failures.push(`${prefix} fighting-character profile requires a model asset, found ${asset.type}.`);
|
|
720
|
+
return { failures, warnings };
|
|
721
|
+
}
|
|
722
|
+
if (asset.format !== "glb" && asset.format !== "gltf") {
|
|
723
|
+
failures.push(`${prefix} fighting-character profile requires GLB/GLTF model input, found ${asset.format}.`);
|
|
724
|
+
}
|
|
725
|
+
if (!licenseVerified) {
|
|
726
|
+
failures.push(`${prefix} fighting-character profile requires verified redistributable license/provenance evidence.`);
|
|
727
|
+
}
|
|
728
|
+
if (!provenance?.sourceUrl && !provenance?.sourceFamily) {
|
|
729
|
+
failures.push(`${prefix} fighting-character profile requires catalog/source provenance for release evidence.`);
|
|
730
|
+
}
|
|
731
|
+
if (asset.animations.length === 0) {
|
|
732
|
+
failures.push(`${prefix} fighting-character profile requires embedded animation clips.`);
|
|
733
|
+
}
|
|
734
|
+
const missing = missingRequiredGameClips(asset.animations);
|
|
735
|
+
if (missing.length > 0) {
|
|
736
|
+
failures.push(`${prefix} fighting-character profile missing required animation clip${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}.`);
|
|
737
|
+
}
|
|
738
|
+
const skeletonJointCount = asset.skeleton?.jointCount ?? 0;
|
|
739
|
+
if (!asset.humanoid?.humanoid && skeletonJointCount < 6) {
|
|
740
|
+
failures.push(`${prefix} fighting-character profile requires humanoid metadata or at least 6 skeleton joints; found ${skeletonJointCount}.`);
|
|
741
|
+
}
|
|
742
|
+
const metadataRisk = findFightingCharacterMetadataRisk(asset);
|
|
743
|
+
if (metadataRisk) {
|
|
744
|
+
failures.push(`${prefix} fighting-character profile rejects ${metadataRisk.kind} metadata "${metadataRisk.term}"; use complete, original, license-safe humanoid fighter assets.`);
|
|
745
|
+
}
|
|
746
|
+
if (!asset.boundsMetadata) {
|
|
747
|
+
failures.push(`${prefix} fighting-character profile requires bounds metadata for scale, ground, and lane checks.`);
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
const bounds = asset.boundsMetadata;
|
|
751
|
+
const height = bounds.size[1];
|
|
752
|
+
if (bounds.maxDimension > 4.5) {
|
|
753
|
+
failures.push(`${prefix} fighting-character profile bounds too large (${bounds.maxDimension}m max); expected character-scale <= 4.5m.`);
|
|
754
|
+
}
|
|
755
|
+
if (height < 0.75) {
|
|
756
|
+
failures.push(`${prefix} fighting-character profile height ${height}m is too small for a readable humanoid fighter.`);
|
|
757
|
+
}
|
|
758
|
+
if (!bounds.grounded) {
|
|
759
|
+
warnings.push(`${prefix} fighting-character profile bounds are not grounded at pivot; min.y is ${bounds.min[1]}m.`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (asset.materials.length === 0) {
|
|
763
|
+
failures.push(`${prefix} fighting-character profile requires at least one readable material.`);
|
|
764
|
+
}
|
|
765
|
+
if (asset.sizeBytes > 50 * 1024 * 1024) {
|
|
766
|
+
failures.push(`${prefix} fighting-character profile payload is ${asset.sizeBytes} bytes; expected <= 52428800 for browser gameplay.`);
|
|
767
|
+
}
|
|
768
|
+
return { failures, warnings };
|
|
769
|
+
}
|
|
770
|
+
function isCharacterLikeAsset(asset) {
|
|
771
|
+
if (asset.humanoid?.humanoid)
|
|
772
|
+
return true;
|
|
773
|
+
return /fighter|player|opponent|enemy|hero|character|avatar|humanoid|npc|body|mara/i.test(asset.id);
|
|
774
|
+
}
|
|
775
|
+
function findFightingCharacterMetadataRisk(asset) {
|
|
776
|
+
const text = [
|
|
777
|
+
asset.id,
|
|
778
|
+
asset.source,
|
|
779
|
+
asset.provenance?.sourceUrl ?? "",
|
|
780
|
+
asset.provenance?.sourceFamily ?? "",
|
|
781
|
+
asset.provenance?.author ?? "",
|
|
782
|
+
...(asset.nodeNames ?? []),
|
|
783
|
+
...asset.materials,
|
|
784
|
+
].join(" ").toLowerCase();
|
|
785
|
+
const ipRiskTerms = [
|
|
786
|
+
"fan art",
|
|
787
|
+
"fanart",
|
|
788
|
+
"copyright",
|
|
789
|
+
"copyrighted",
|
|
790
|
+
"ripped",
|
|
791
|
+
"pokemon",
|
|
792
|
+
"mario",
|
|
793
|
+
"sonic",
|
|
794
|
+
"naruto",
|
|
795
|
+
"dragon ball",
|
|
796
|
+
"fortnite",
|
|
797
|
+
"marvel",
|
|
798
|
+
"dc comics",
|
|
799
|
+
"star wars",
|
|
800
|
+
"disney",
|
|
801
|
+
];
|
|
802
|
+
const ipRisk = ipRiskTerms.find((term) => text.includes(term));
|
|
803
|
+
if (ipRisk)
|
|
804
|
+
return { kind: "IP-risk", term: ipRisk };
|
|
805
|
+
const nonCharacterTerms = [
|
|
806
|
+
"aircraft",
|
|
807
|
+
"airplane",
|
|
808
|
+
"vehicle",
|
|
809
|
+
"building",
|
|
810
|
+
"architecture",
|
|
811
|
+
"environment",
|
|
812
|
+
"terrain",
|
|
813
|
+
"prop",
|
|
814
|
+
"furniture",
|
|
815
|
+
"sculpt",
|
|
816
|
+
"sculpture",
|
|
817
|
+
"statue",
|
|
818
|
+
"bust",
|
|
819
|
+
"figurine",
|
|
820
|
+
"miniature",
|
|
821
|
+
"photogrammetry",
|
|
822
|
+
"pedestal",
|
|
823
|
+
"spider",
|
|
824
|
+
"animal",
|
|
825
|
+
"quadruped",
|
|
826
|
+
"creature",
|
|
827
|
+
"insect",
|
|
828
|
+
"dragon",
|
|
829
|
+
"dinosaur",
|
|
830
|
+
"horse",
|
|
831
|
+
"dog",
|
|
832
|
+
"cat",
|
|
833
|
+
"bird",
|
|
834
|
+
"fish",
|
|
835
|
+
];
|
|
836
|
+
const nonCharacter = nonCharacterTerms.find((term) => text.includes(term));
|
|
837
|
+
if (nonCharacter)
|
|
838
|
+
return { kind: "non-character", term: nonCharacter };
|
|
839
|
+
return undefined;
|
|
840
|
+
}
|
|
841
|
+
function missingRequiredGameClips(animations) {
|
|
842
|
+
const normalized = animations.map((name) => name.toLowerCase().replace(/[^a-z0-9]/g, ""));
|
|
843
|
+
const hasNamed = (patterns) => normalized.some((name) => patterns.some((pattern) => pattern.test(name)));
|
|
844
|
+
const missing = [];
|
|
845
|
+
if (!hasNamed([/idle/, /stand/]))
|
|
846
|
+
missing.push("idle");
|
|
847
|
+
if (!hasNamed([/walk/, /locomotion/, /move/]))
|
|
848
|
+
missing.push("walk");
|
|
849
|
+
if (!hasNamed([/lightpunch/, /lightattack/, /light/, /jab/, /punch/, /attack/]))
|
|
850
|
+
missing.push("lightPunch");
|
|
851
|
+
return missing;
|
|
852
|
+
}
|
|
853
|
+
function hasFloatingHairRisk(asset) {
|
|
854
|
+
const names = [asset.id, ...(asset.nodeNames ?? [])].join(" ").toLowerCase();
|
|
855
|
+
if (!names.includes("hair"))
|
|
856
|
+
return false;
|
|
857
|
+
const hasBodyAnchor = /body|torso|spine|chest|head|neck|skull|face|hips|pelvis/.test(names);
|
|
858
|
+
return !hasBodyAnchor;
|
|
859
|
+
}
|
|
860
|
+
function pushUnique(target, values) {
|
|
861
|
+
for (const value of values) {
|
|
862
|
+
if (!target.includes(value))
|
|
863
|
+
target.push(value);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
function resolvePublicArtifactPath(projectDir, manifest, url) {
|
|
867
|
+
if (/^https?:\/\//i.test(url))
|
|
868
|
+
return undefined;
|
|
869
|
+
if (url.startsWith(manifest.assetBasePath)) {
|
|
870
|
+
return resolve(projectDir, manifest.outputDir, url.slice(manifest.assetBasePath.length));
|
|
871
|
+
}
|
|
872
|
+
return resolve(projectDir, url.replace(/^\//, "public/"));
|
|
873
|
+
}
|
|
874
|
+
function defaultAttachPoint(slot) {
|
|
875
|
+
const normalized = slot.toLowerCase();
|
|
876
|
+
if (normalized.includes("hair") || normalized.includes("hat") || normalized.includes("face"))
|
|
877
|
+
return "head";
|
|
878
|
+
if (normalized.includes("hand") || normalized.includes("weapon") || normalized.includes("prop"))
|
|
879
|
+
return "rightHand";
|
|
880
|
+
if (normalized.includes("shoe") || normalized.includes("boot"))
|
|
881
|
+
return "feet";
|
|
882
|
+
if (normalized.includes("cape") || normalized.includes("back"))
|
|
883
|
+
return "spine";
|
|
884
|
+
return "root";
|
|
885
|
+
}
|
|
247
886
|
export function readAssetManifest(projectDir) {
|
|
248
887
|
const manifestPath = resolve(projectDir, DEFAULT_AURA_ASSET_MANIFEST);
|
|
249
888
|
if (!existsSync(manifestPath)) {
|
|
@@ -270,8 +909,15 @@ function inspectAssetFile(path, format) {
|
|
|
270
909
|
return inspectGlb(readFileSync(path), dirname(path));
|
|
271
910
|
return {
|
|
272
911
|
materials: [],
|
|
912
|
+
materialMetadata: [],
|
|
273
913
|
animations: [],
|
|
914
|
+
animation: emptyAnimationInspection(),
|
|
915
|
+
humanoid: unknownHumanoidInspection("Humanoid detection is only available for GLB/glTF model assets."),
|
|
916
|
+
skeleton: emptySkeletonInspection("Skeleton detection is only available for GLB/glTF model assets."),
|
|
917
|
+
morphTargets: emptyMorphTargetInspection("Morph target detection is only available for GLB/glTF model assets."),
|
|
274
918
|
textures: [],
|
|
919
|
+
orientation: unknownOrientationInspection(),
|
|
920
|
+
nodeNames: [],
|
|
275
921
|
dependencies: [],
|
|
276
922
|
bounds: undefined
|
|
277
923
|
};
|
|
@@ -300,18 +946,226 @@ function inspectGltf(json, baseDir) {
|
|
|
300
946
|
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
947
|
}
|
|
302
948
|
}
|
|
949
|
+
const boundsMetadata = extractBoundsDetails(json);
|
|
303
950
|
return {
|
|
304
|
-
bounds:
|
|
951
|
+
bounds: boundsMetadata?.size,
|
|
952
|
+
boundsMetadata,
|
|
305
953
|
materials: (json.materials ?? []).map((material, index) => material.name ?? `material-${index}`),
|
|
954
|
+
materialMetadata: inspectGltfMaterials(json),
|
|
306
955
|
animations: (json.animations ?? []).map((animation, index) => animation.name ?? `clip-${index}`),
|
|
956
|
+
animation: inspectGltfAnimations(json),
|
|
957
|
+
humanoid: inspectGltfHumanoid(json),
|
|
958
|
+
skeleton: inspectGltfSkeleton(json),
|
|
959
|
+
morphTargets: inspectGltfMorphTargets(json),
|
|
960
|
+
provenance: inspectGltfProvenance(json),
|
|
307
961
|
textures: (json.images ?? []).map((image, index) => image.uri ?? image.name ?? `image-${index}`),
|
|
962
|
+
orientation: inspectGltfOrientation(json),
|
|
963
|
+
nodeNames: (json.nodes ?? []).map((node, index) => node.name ?? `node-${index}`),
|
|
308
964
|
dependencies
|
|
309
965
|
};
|
|
310
966
|
}
|
|
967
|
+
function emptyAnimationInspection() {
|
|
968
|
+
return {
|
|
969
|
+
clipCount: 0,
|
|
970
|
+
clips: [],
|
|
971
|
+
messages: ["No embedded animation clips detected."]
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
function inspectGltfAnimations(json) {
|
|
975
|
+
const clips = (json.animations ?? []).map((animation, index) => {
|
|
976
|
+
const channels = animation.channels ?? [];
|
|
977
|
+
const targetPaths = uniqueStrings(channels.map((channel) => channel.target?.path).filter(isString));
|
|
978
|
+
const targetNodes = uniqueStrings(channels.map((channel) => {
|
|
979
|
+
const nodeIndex = channel.target?.node;
|
|
980
|
+
return typeof nodeIndex === "number" ? json.nodes?.[nodeIndex]?.name ?? `node-${nodeIndex}` : undefined;
|
|
981
|
+
}).filter(isString));
|
|
982
|
+
return {
|
|
983
|
+
index,
|
|
984
|
+
name: animation.name ?? `clip-${index}`,
|
|
985
|
+
channelCount: channels.length,
|
|
986
|
+
samplerCount: animation.samplers?.length ?? 0,
|
|
987
|
+
targetPaths,
|
|
988
|
+
targetNodes
|
|
989
|
+
};
|
|
990
|
+
});
|
|
991
|
+
return {
|
|
992
|
+
clipCount: clips.length,
|
|
993
|
+
clips,
|
|
994
|
+
messages: clips.length === 0
|
|
995
|
+
? ["No embedded animation clips detected."]
|
|
996
|
+
: [`Detected ${clips.length} embedded animation clip${clips.length === 1 ? "" : "s"}.`]
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function inspectGltfHumanoid(json) {
|
|
1000
|
+
const skinCount = json.skins?.length ?? 0;
|
|
1001
|
+
const jointIndexes = uniqueNumbers((json.skins ?? []).flatMap((skin) => skin.joints ?? []));
|
|
1002
|
+
const jointNames = uniqueStrings(jointIndexes.map((index) => json.nodes?.[index]?.name ?? `joint-${index}`));
|
|
1003
|
+
const nodeNames = uniqueStrings((json.nodes ?? []).map((node, index) => node.name ?? `node-${index}`));
|
|
1004
|
+
const candidates = jointNames.length > 0 ? jointNames : nodeNames;
|
|
1005
|
+
const requiredBones = ["hips", "spine", "head", "leftArm", "rightArm", "leftLeg", "rightLeg"];
|
|
1006
|
+
const matchedBones = requiredBones.filter((bone) => candidates.some((name) => matchesHumanoidBone(name, bone)));
|
|
1007
|
+
const missingBones = requiredBones.filter((bone) => !matchedBones.includes(bone));
|
|
1008
|
+
const hasSkin = jointIndexes.length > 0;
|
|
1009
|
+
const hasTorso = matchedBones.includes("hips") && matchedBones.includes("spine") && matchedBones.includes("head");
|
|
1010
|
+
const hasArms = matchedBones.includes("leftArm") && matchedBones.includes("rightArm");
|
|
1011
|
+
const hasLegs = matchedBones.includes("leftLeg") && matchedBones.includes("rightLeg");
|
|
1012
|
+
const humanoid = (hasSkin && hasTorso && hasArms && hasLegs) || (!hasSkin && hasTorso && matchedBones.length >= 5);
|
|
1013
|
+
const status = humanoid
|
|
1014
|
+
? "humanoid"
|
|
1015
|
+
: hasSkin || matchedBones.length > 0
|
|
1016
|
+
? "unknown"
|
|
1017
|
+
: "non-humanoid";
|
|
1018
|
+
const confidence = humanoid && hasSkin
|
|
1019
|
+
? "high"
|
|
1020
|
+
: humanoid || (hasSkin && matchedBones.length >= 5)
|
|
1021
|
+
? "medium"
|
|
1022
|
+
: "low";
|
|
1023
|
+
return {
|
|
1024
|
+
humanoid,
|
|
1025
|
+
status,
|
|
1026
|
+
confidence,
|
|
1027
|
+
skinCount,
|
|
1028
|
+
jointCount: jointIndexes.length,
|
|
1029
|
+
matchedBones,
|
|
1030
|
+
missingBones,
|
|
1031
|
+
messages: humanoid
|
|
1032
|
+
? [`Humanoid signals detected from ${hasSkin ? "skinned joints" : "node names"}.`]
|
|
1033
|
+
: status === "unknown"
|
|
1034
|
+
? [`Humanoid status is unknown; missing bone groups: ${missingBones.join(", ")}.`]
|
|
1035
|
+
: ["No humanoid skeleton signals detected."]
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function inspectGltfSkeleton(json) {
|
|
1039
|
+
const skins = (json.skins ?? []).map((skin, index) => {
|
|
1040
|
+
const joints = (skin.joints ?? []).map((jointIndex) => json.nodes?.[jointIndex]?.name ?? `joint-${jointIndex}`);
|
|
1041
|
+
const skeletonIndex = skin.skeleton;
|
|
1042
|
+
return {
|
|
1043
|
+
index,
|
|
1044
|
+
name: skin.name ?? `skin-${index}`,
|
|
1045
|
+
jointCount: joints.length,
|
|
1046
|
+
joints,
|
|
1047
|
+
...(typeof skeletonIndex === "number" ? { skeleton: json.nodes?.[skeletonIndex]?.name ?? `node-${skeletonIndex}` } : {})
|
|
1048
|
+
};
|
|
1049
|
+
});
|
|
1050
|
+
const jointCount = uniqueStrings(skins.flatMap((skin) => skin.joints)).length;
|
|
1051
|
+
return {
|
|
1052
|
+
skinCount: skins.length,
|
|
1053
|
+
jointCount,
|
|
1054
|
+
skins,
|
|
1055
|
+
messages: skins.length === 0
|
|
1056
|
+
? ["No skin/skeleton metadata detected."]
|
|
1057
|
+
: [`Detected ${skins.length} skin${skins.length === 1 ? "" : "s"} with ${jointCount} unique joint${jointCount === 1 ? "" : "s"}.`]
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function emptySkeletonInspection(message) {
|
|
1061
|
+
return {
|
|
1062
|
+
skinCount: 0,
|
|
1063
|
+
jointCount: 0,
|
|
1064
|
+
skins: [],
|
|
1065
|
+
messages: [message]
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function inspectGltfMorphTargets(json) {
|
|
1069
|
+
const meshes = (json.meshes ?? []).map((mesh, index) => {
|
|
1070
|
+
const meshExtras = objectValue(mesh.extras);
|
|
1071
|
+
const namedTargets = stringArrayValue(meshExtras?.targetNames ?? meshExtras?.morphTargetNames);
|
|
1072
|
+
const targetCount = Math.max(namedTargets.length, mesh.weights?.length ?? 0, ...(mesh.primitives ?? []).map((primitive) => primitive.targets?.length ?? 0));
|
|
1073
|
+
if (targetCount === 0)
|
|
1074
|
+
return undefined;
|
|
1075
|
+
const targetNames = targetCount > 0
|
|
1076
|
+
? Array.from({ length: targetCount }, (_, targetIndex) => namedTargets[targetIndex] ?? `target-${targetIndex}`)
|
|
1077
|
+
: [];
|
|
1078
|
+
return {
|
|
1079
|
+
index,
|
|
1080
|
+
name: mesh.name ?? `mesh-${index}`,
|
|
1081
|
+
targetNames
|
|
1082
|
+
};
|
|
1083
|
+
}).filter((mesh) => Boolean(mesh));
|
|
1084
|
+
const targetNames = uniqueStrings(meshes.flatMap((mesh) => mesh.targetNames));
|
|
1085
|
+
return {
|
|
1086
|
+
targetCount: targetNames.length,
|
|
1087
|
+
targetNames,
|
|
1088
|
+
meshes,
|
|
1089
|
+
messages: targetNames.length === 0
|
|
1090
|
+
? ["No morph target metadata detected."]
|
|
1091
|
+
: [`Detected ${targetNames.length} morph target${targetNames.length === 1 ? "" : "s"}.`]
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function emptyMorphTargetInspection(message) {
|
|
1095
|
+
return {
|
|
1096
|
+
targetCount: 0,
|
|
1097
|
+
targetNames: [],
|
|
1098
|
+
meshes: [],
|
|
1099
|
+
messages: [message]
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function inspectGltfProvenance(json) {
|
|
1103
|
+
const assetExtras = objectValue(json.asset?.extras);
|
|
1104
|
+
const auraExtras = objectValue(assetExtras?.aura3d) ?? assetExtras;
|
|
1105
|
+
const provenance = objectValue(auraExtras?.provenance ?? auraExtras?.license ?? auraExtras?.source);
|
|
1106
|
+
const sourceUrl = stringValue(provenance?.sourceUrl ?? provenance?.url ?? auraExtras?.sourceUrl);
|
|
1107
|
+
const license = stringValue(provenance?.license ?? provenance?.spdx ?? auraExtras?.license);
|
|
1108
|
+
const author = stringValue(provenance?.author ?? provenance?.creator ?? auraExtras?.author);
|
|
1109
|
+
const sourceFamily = stringValue(provenance?.sourceFamily ?? provenance?.source ?? auraExtras?.sourceFamily);
|
|
1110
|
+
const attribution = stringValue(provenance?.attribution ?? auraExtras?.attribution);
|
|
1111
|
+
const evidence = stringArrayValue(provenance?.evidence ?? auraExtras?.evidence);
|
|
1112
|
+
if (!sourceUrl && !license && !author && !sourceFamily && !attribution && evidence.length === 0)
|
|
1113
|
+
return undefined;
|
|
1114
|
+
return {
|
|
1115
|
+
...(sourceUrl ? { sourceUrl } : {}),
|
|
1116
|
+
...(license ? { license } : {}),
|
|
1117
|
+
...(author ? { author } : {}),
|
|
1118
|
+
...(sourceFamily ? { sourceFamily } : {}),
|
|
1119
|
+
...(attribution ? { attribution } : {}),
|
|
1120
|
+
...(evidence.length > 0 ? { evidence } : {})
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
function unknownHumanoidInspection(message) {
|
|
1124
|
+
return {
|
|
1125
|
+
humanoid: false,
|
|
1126
|
+
status: "unknown",
|
|
1127
|
+
confidence: "low",
|
|
1128
|
+
skinCount: 0,
|
|
1129
|
+
jointCount: 0,
|
|
1130
|
+
matchedBones: [],
|
|
1131
|
+
missingBones: ["hips", "spine", "head", "leftArm", "rightArm", "leftLeg", "rightLeg"],
|
|
1132
|
+
messages: [message]
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function matchesHumanoidBone(name, bone) {
|
|
1136
|
+
const raw = name.toLowerCase();
|
|
1137
|
+
const compact = raw.replace(/[^a-z0-9]/g, "");
|
|
1138
|
+
const left = compact.includes("left") || /(^|[^a-z0-9])l([^a-z0-9]|$)/.test(raw) || /[^a-z0-9]l$/.test(raw);
|
|
1139
|
+
const right = compact.includes("right") || /(^|[^a-z0-9])r([^a-z0-9]|$)/.test(raw) || /[^a-z0-9]r$/.test(raw);
|
|
1140
|
+
const arm = compact.includes("arm") || compact.includes("forearm") || compact.includes("shoulder") || compact.includes("hand");
|
|
1141
|
+
const leg = compact.includes("leg") || compact.includes("thigh") || compact.includes("foot") || compact.includes("toe");
|
|
1142
|
+
if (bone === "hips")
|
|
1143
|
+
return compact.includes("hip") || compact.includes("pelvis");
|
|
1144
|
+
if (bone === "spine")
|
|
1145
|
+
return compact.includes("spine") || compact.includes("chest") || compact.includes("torso");
|
|
1146
|
+
if (bone === "head")
|
|
1147
|
+
return compact.includes("head") || compact.includes("neck");
|
|
1148
|
+
if (bone === "leftArm")
|
|
1149
|
+
return left && arm;
|
|
1150
|
+
if (bone === "rightArm")
|
|
1151
|
+
return right && arm;
|
|
1152
|
+
if (bone === "leftLeg")
|
|
1153
|
+
return left && leg;
|
|
1154
|
+
return right && leg;
|
|
1155
|
+
}
|
|
311
1156
|
function isExternalUri(uri) {
|
|
312
1157
|
return typeof uri === "string" && uri.length > 0 && !uri.startsWith("data:");
|
|
313
1158
|
}
|
|
314
|
-
function
|
|
1159
|
+
function isString(value) {
|
|
1160
|
+
return typeof value === "string";
|
|
1161
|
+
}
|
|
1162
|
+
function uniqueStrings(values) {
|
|
1163
|
+
return [...new Set(values)];
|
|
1164
|
+
}
|
|
1165
|
+
function uniqueNumbers(values) {
|
|
1166
|
+
return [...new Set(values)];
|
|
1167
|
+
}
|
|
1168
|
+
function extractBoundsDetails(json) {
|
|
315
1169
|
let min;
|
|
316
1170
|
let max;
|
|
317
1171
|
for (const accessor of json.accessors ?? []) {
|
|
@@ -320,7 +1174,89 @@ function extractBounds(json) {
|
|
|
320
1174
|
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
1175
|
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
1176
|
}
|
|
323
|
-
|
|
1177
|
+
if (!min || !max)
|
|
1178
|
+
return undefined;
|
|
1179
|
+
const size = [round(max[0] - min[0]), round(max[1] - min[1]), round(max[2] - min[2])];
|
|
1180
|
+
const center = [round((min[0] + max[0]) / 2), round((min[1] + max[1]) / 2), round((min[2] + max[2]) / 2)];
|
|
1181
|
+
const roundedMin = [round(min[0]), round(min[1]), round(min[2])];
|
|
1182
|
+
const roundedMax = [round(max[0]), round(max[1]), round(max[2])];
|
|
1183
|
+
return {
|
|
1184
|
+
min: roundedMin,
|
|
1185
|
+
max: roundedMax,
|
|
1186
|
+
size,
|
|
1187
|
+
center,
|
|
1188
|
+
maxDimension: Math.max(...size),
|
|
1189
|
+
grounded: Math.abs(roundedMin[1]) <= 0.08
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
function inspectGltfMaterials(json) {
|
|
1193
|
+
return (json.materials ?? []).map((material, index) => {
|
|
1194
|
+
const name = material.name ?? `material-${index}`;
|
|
1195
|
+
const extras = objectValue(material.extras);
|
|
1196
|
+
const explicitVisible = booleanValue(extras?.visible);
|
|
1197
|
+
const explicitReadable = booleanValue(extras?.readable);
|
|
1198
|
+
const opacity = round(numberValue(material.pbrMetallicRoughness?.baseColorFactor?.[3]) ?? 1);
|
|
1199
|
+
const alphaMode = material.alphaMode;
|
|
1200
|
+
const alphaCutoff = numberValue(material.alphaCutoff);
|
|
1201
|
+
const reasons = [];
|
|
1202
|
+
if (opacity <= 0)
|
|
1203
|
+
reasons.push("baseColorFactor alpha is 0");
|
|
1204
|
+
if (alphaMode === "MASK" && (alphaCutoff ?? 0.5) >= 1)
|
|
1205
|
+
reasons.push("alpha mask cutoff hides fully transparent surfaces");
|
|
1206
|
+
if (explicitVisible === false)
|
|
1207
|
+
reasons.push("material extras mark visible=false");
|
|
1208
|
+
if (explicitReadable === false)
|
|
1209
|
+
reasons.push("material extras mark readable=false");
|
|
1210
|
+
const visible = explicitVisible ?? (opacity > 0 && !(alphaMode === "MASK" && (alphaCutoff ?? 0.5) >= 1));
|
|
1211
|
+
const readable = explicitReadable ?? visible;
|
|
1212
|
+
return {
|
|
1213
|
+
name,
|
|
1214
|
+
visible,
|
|
1215
|
+
readable,
|
|
1216
|
+
opacity,
|
|
1217
|
+
...(alphaMode ? { alphaMode } : {}),
|
|
1218
|
+
reasons
|
|
1219
|
+
};
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
function inspectGltfOrientation(json) {
|
|
1223
|
+
const assetExtras = objectValue(json.asset?.extras);
|
|
1224
|
+
const auraExtras = objectValue(assetExtras?.aura3d) ?? assetExtras;
|
|
1225
|
+
const orientation = objectValue(auraExtras?.orientation) ?? auraExtras;
|
|
1226
|
+
const forwardAxis = stringValue(orientation?.forwardAxis ?? orientation?.forward ?? orientation?.facing);
|
|
1227
|
+
const upAxis = stringValue(orientation?.upAxis ?? orientation?.up);
|
|
1228
|
+
if (!forwardAxis && !upAxis)
|
|
1229
|
+
return unknownOrientationInspection();
|
|
1230
|
+
return {
|
|
1231
|
+
source: "gltf-extras",
|
|
1232
|
+
...(forwardAxis ? { forwardAxis } : {}),
|
|
1233
|
+
...(upAxis ? { upAxis } : {}),
|
|
1234
|
+
messages: [`Orientation metadata detected${forwardAxis ? ` with forwardAxis=${forwardAxis}` : ""}${upAxis ? ` and upAxis=${upAxis}` : ""}.`]
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
function unknownOrientationInspection() {
|
|
1238
|
+
return {
|
|
1239
|
+
source: "unknown",
|
|
1240
|
+
messages: ["No orientation metadata detected; facing direction cannot be proven."]
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function objectValue(value) {
|
|
1244
|
+
return typeof value === "object" && value !== null ? value : undefined;
|
|
1245
|
+
}
|
|
1246
|
+
function stringValue(value) {
|
|
1247
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1248
|
+
}
|
|
1249
|
+
function stringArrayValue(value) {
|
|
1250
|
+
if (!Array.isArray(value))
|
|
1251
|
+
return [];
|
|
1252
|
+
return value.map((entry) => stringValue(entry)).filter((entry) => Boolean(entry));
|
|
1253
|
+
}
|
|
1254
|
+
function booleanValue(value) {
|
|
1255
|
+
return typeof value === "boolean" ? value : undefined;
|
|
1256
|
+
}
|
|
1257
|
+
function numberValue(value) {
|
|
1258
|
+
const number = Number(value);
|
|
1259
|
+
return Number.isFinite(number) ? number : undefined;
|
|
324
1260
|
}
|
|
325
1261
|
function createAssetWarnings(path, inspection) {
|
|
326
1262
|
const warnings = [];
|
|
@@ -331,8 +1267,96 @@ function createAssetWarnings(path, inspection) {
|
|
|
331
1267
|
warnings.push("bounds could not be extracted");
|
|
332
1268
|
if (inspection.textures.length === 0 && ["glb", "gltf"].includes(extname(path).slice(1).toLowerCase()))
|
|
333
1269
|
warnings.push("no texture references detected");
|
|
1270
|
+
if (inspection.orientation.source === "unknown" && ["glb", "gltf"].includes(extname(path).slice(1).toLowerCase()))
|
|
1271
|
+
warnings.push("orientation metadata missing; facing direction cannot be validated until GLTF extras declare aura3d.orientation.forwardAxis");
|
|
1272
|
+
if (inspection.materialMetadata.some((material) => !material.visible || !material.readable))
|
|
1273
|
+
warnings.push("one or more materials are invisible or unreadable");
|
|
334
1274
|
return warnings;
|
|
335
1275
|
}
|
|
1276
|
+
function createAssetProvenance(projectDir, sourcePath, options, detected) {
|
|
1277
|
+
return {
|
|
1278
|
+
sourcePath: normalizeRelativePath(relative(projectDir, sourcePath)),
|
|
1279
|
+
...(options.sourceUrl ?? detected?.sourceUrl ? { sourceUrl: options.sourceUrl ?? detected?.sourceUrl } : {}),
|
|
1280
|
+
...(options.license ?? detected?.license ? { license: options.license ?? detected?.license } : {}),
|
|
1281
|
+
...(options.author ?? detected?.author ? { author: options.author ?? detected?.author } : {}),
|
|
1282
|
+
...(options.sourceFamily ?? detected?.sourceFamily ? { sourceFamily: options.sourceFamily ?? detected?.sourceFamily } : {}),
|
|
1283
|
+
...(options.attribution ?? detected?.attribution ? { attribution: options.attribution ?? detected?.attribution } : {}),
|
|
1284
|
+
...(detected?.evidence && detected.evidence.length > 0 ? { evidence: detected.evidence } : {}),
|
|
1285
|
+
checkedAt: new Date().toISOString()
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function readExternalProvenance(projectDir, provenanceFile) {
|
|
1289
|
+
if (!provenanceFile)
|
|
1290
|
+
return new Map();
|
|
1291
|
+
const path = resolve(projectDir, provenanceFile);
|
|
1292
|
+
if (!existsSync(path))
|
|
1293
|
+
return new Map();
|
|
1294
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1295
|
+
const root = objectValue(parsed);
|
|
1296
|
+
if (!root)
|
|
1297
|
+
return new Map();
|
|
1298
|
+
const checkedAt = stringValue(root.updatedAt ?? root.verifiedAt ?? root.checkedAt) ?? new Date().toISOString();
|
|
1299
|
+
const records = [
|
|
1300
|
+
...arrayObjectValue(root.launchGlbs),
|
|
1301
|
+
...arrayObjectValue(root.assets),
|
|
1302
|
+
...arrayObjectValue(root.assetEvidence)
|
|
1303
|
+
];
|
|
1304
|
+
const byId = new Map();
|
|
1305
|
+
for (const record of records) {
|
|
1306
|
+
const typedAsset = stringValue(record.typedAsset);
|
|
1307
|
+
const id = stringValue(record.assetKey ?? record.id) ?? typedAsset?.replace(/^assets\./, "");
|
|
1308
|
+
if (!id)
|
|
1309
|
+
continue;
|
|
1310
|
+
const nestedProvenance = objectValue(record.provenance);
|
|
1311
|
+
const sourcePath = stringValue(record.sourcePath ?? record.source ?? nestedProvenance?.builderOutput) ?? id;
|
|
1312
|
+
const license = stringValue(record.license ?? record.licenseNote ?? nestedProvenance?.license);
|
|
1313
|
+
const sourceUrl = stringValue(record.sourceUrl ?? record.publicUrl ?? record.officialPage ?? nestedProvenance?.sourceUrl);
|
|
1314
|
+
const sourceFamily = stringValue(record.sourceFamily ?? nestedProvenance?.sourceFamily ?? nestedProvenance?.sourcePack);
|
|
1315
|
+
const author = stringValue(record.author ?? nestedProvenance?.author);
|
|
1316
|
+
const attribution = stringValue(record.attribution ?? record.credit ?? nestedProvenance?.attribution);
|
|
1317
|
+
const evidence = [
|
|
1318
|
+
...stringArrayValue(record.evidence),
|
|
1319
|
+
...stringArrayValue(record.intendedRouteUsage),
|
|
1320
|
+
...stringArrayValue(nestedProvenance?.evidence)
|
|
1321
|
+
];
|
|
1322
|
+
byId.set(id, {
|
|
1323
|
+
sourcePath,
|
|
1324
|
+
...(sourceUrl ? { sourceUrl } : {}),
|
|
1325
|
+
...(license ? { license } : {}),
|
|
1326
|
+
...(author ? { author } : {}),
|
|
1327
|
+
...(sourceFamily ? { sourceFamily } : {}),
|
|
1328
|
+
...(attribution ? { attribution } : {}),
|
|
1329
|
+
...(evidence.length > 0 ? { evidence } : {}),
|
|
1330
|
+
checkedAt
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
return byId;
|
|
1334
|
+
}
|
|
1335
|
+
function arrayObjectValue(value) {
|
|
1336
|
+
if (!Array.isArray(value))
|
|
1337
|
+
return [];
|
|
1338
|
+
return value.map((entry) => objectValue(entry)).filter((entry) => Boolean(entry));
|
|
1339
|
+
}
|
|
1340
|
+
function resolveAssetProvenance(asset, externalProvenance) {
|
|
1341
|
+
return asset.provenance ?? externalProvenance.get(asset.id);
|
|
1342
|
+
}
|
|
1343
|
+
function hasUsableLicenseEvidence(provenance) {
|
|
1344
|
+
const license = provenance?.license?.trim();
|
|
1345
|
+
if (!license)
|
|
1346
|
+
return false;
|
|
1347
|
+
return !/(unverified|unknown|candidate|needs[-\s]?confirmation|todo|placeholder)/i.test(license);
|
|
1348
|
+
}
|
|
1349
|
+
function isPlaceholderAsset(asset, provenance) {
|
|
1350
|
+
const value = [
|
|
1351
|
+
asset.id,
|
|
1352
|
+
asset.source,
|
|
1353
|
+
asset.outputPath,
|
|
1354
|
+
asset.url,
|
|
1355
|
+
provenance?.sourcePath,
|
|
1356
|
+
provenance?.sourceUrl
|
|
1357
|
+
].filter(Boolean).join(" ");
|
|
1358
|
+
return /(^|[-_./\s])(placeholder|dummy|mock|todo|replace-me|sample-placeholder)([-_./\s]|$)/i.test(value);
|
|
1359
|
+
}
|
|
336
1360
|
function copyAssetDependencies(projectDir, sourcePath, outputDir, dependencies) {
|
|
337
1361
|
const sourceDir = dirname(sourcePath);
|
|
338
1362
|
for (const dependency of dependencies) {
|