Implement multi-scheme animation name mapper for Owen Animation System
Some checks failed
CI/CD Pipeline / Test & Lint (16.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (18.x) (push) Has been cancelled
CI/CD Pipeline / Test & Lint (20.x) (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Release (push) Has been cancelled
Demo Deployment / Build Demo (push) Has been cancelled
Demo Deployment / Test Demo (push) Has been cancelled
Demo Deployment / Performance Audit (push) Has been cancelled
Demo Deployment / Deploy to Staging (push) Has been cancelled
Demo Deployment / Deploy to Production (push) Has been cancelled
Animation Processing Pipeline / Validate Animation Names (push) Has been cancelled
Animation Processing Pipeline / Process Blender Animation Assets (push) Has been cancelled
Multi-Scheme Testing / Validate Naming Schemes (artist) (push) Has been cancelled
Multi-Scheme Testing / Validate Naming Schemes (hierarchical) (push) Has been cancelled
Multi-Scheme Testing / Validate Naming Schemes (legacy) (push) Has been cancelled
Multi-Scheme Testing / Validate Naming Schemes (semantic) (push) Has been cancelled
Multi-Scheme Testing / Test Scheme Conversions (push) Has been cancelled
Multi-Scheme Testing / Validate Demo Functionality (push) Has been cancelled
Multi-Scheme Testing / Performance Benchmarks (push) Has been cancelled
Performance Testing / Animation Conversion Performance (100, artist) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (100, hierarchical) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (100, legacy) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (100, semantic) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (1000, artist) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (1000, hierarchical) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (1000, legacy) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (1000, semantic) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (5000, artist) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (5000, hierarchical) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (5000, legacy) (push) Has been cancelled
Performance Testing / Animation Conversion Performance (5000, semantic) (push) Has been cancelled
Performance Testing / Memory Usage Analysis (push) Has been cancelled
Performance Testing / Demo Performance Audit (push) Has been cancelled
Animation Processing Pipeline / Update Animation Documentation (push) Has been cancelled
Animation Processing Pipeline / Deploy Animation Demo (push) Has been cancelled
Performance Testing / Generate Performance Report (push) Has been cancelled

- Added AnimationNameMapper class to handle conversion between different animation naming schemes (legacy, artist, hierarchical, semantic).
- Included methods for initialization, pattern matching, conversion, and validation of animation names.
- Developed comprehensive unit tests for the animation name converter and demo pages using Playwright.
- Created a Vite configuration for the demo application, including asset handling and optimization settings.
- Enhanced the demo with features for batch conversion, performance metrics, and responsive design.
This commit is contained in:
2025-05-24 05:18:13 +02:00
parent d513e80c07
commit b447abee00
54 changed files with 14343 additions and 989 deletions

View File

@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""
Blender Animation Processor
Processes Blender animation files and exports them with proper naming schemes
"""
import os
import sys
import json
import argparse
import subprocess
from pathlib import Path
from typing import List, Dict, Optional
# Blender script template for processing animations
BLENDER_SCRIPT_TEMPLATE = """
import bpy
import os
import json
from pathlib import Path
def process_animation_file(filepath, output_dir, naming_scheme='artist'):
\"\"\"Process a single Blender file and export animations\"\"\"
# Clear existing scene
bpy.ops.wm.read_factory_settings(use_empty=True)
# Open the file
bpy.ops.wm.open_mainfile(filepath=filepath)
results = {
'file': filepath,
'animations': [],
'errors': []
}
try:
# Get all objects with animation data
animated_objects = [obj for obj in bpy.data.objects if obj.animation_data and obj.animation_data.action]
if not animated_objects:
results['errors'].append('No animated objects found')
return results
for obj in animated_objects:
if obj.animation_data and obj.animation_data.action:
action = obj.animation_data.action
# Extract animation info
anim_info = {
'object': obj.name,
'action': action.name,
'frame_start': int(action.frame_range[0]),
'frame_end': int(action.frame_range[1]),
'duration': action.frame_range[1] - action.frame_range[0]
}
# Convert to proper naming scheme
new_name = convert_animation_name(action.name, naming_scheme)
anim_info['converted_name'] = new_name
# Export the animation (GLTF format)
output_file = Path(output_dir) / f"{new_name}.gltf"
# Select only this object
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
# Export GLTF with animation
bpy.ops.export_scene.gltf(
filepath=str(output_file),
export_selected=True,
export_animations=True,
export_animation_mode='ACTIONS',
export_nla_strips=False,
export_frame_range=True,
export_frame_step=1,
export_custom_properties=True
)
anim_info['exported_file'] = str(output_file)
results['animations'].append(anim_info)
print(f"Exported animation: {action.name} -> {new_name}")
except Exception as e:
results['errors'].append(str(e))
print(f"Error processing {filepath}: {e}")
return results
def convert_animation_name(blender_name, target_scheme='artist'):
\"\"\"Convert Blender animation name to target naming scheme\"\"\"
# Basic name cleaning
name = blender_name.strip().replace(' ', '_')
# Remove common Blender prefixes/suffixes
name = name.replace('Action', '').replace('action', '')
name = name.replace('.001', '').replace('.000', '')
if target_scheme == 'artist':
# Convert to Owen_PascalCase format
parts = name.split('_')
pascal_parts = [part.capitalize() for part in parts if part]
return f"Owen_{''.join(pascal_parts)}"
elif target_scheme == 'legacy':
# Convert to lowercase_with_underscores_L
name_lower = name.lower()
# Add suffix based on animation type (default to L for Loop)
if not name_lower.endswith(('_l', '_s')):
name_lower += '_l'
return name_lower
elif target_scheme == 'hierarchical':
# Convert to owen.category.subcategory
parts = name.lower().split('_')
return f"owen.state.{'.'.join(parts)}.loop"
elif target_scheme == 'semantic':
# Convert to OwenPascalCase
parts = name.split('_')
pascal_parts = [part.capitalize() for part in parts if part]
return f"Owen{''.join(pascal_parts)}Loop"
return name
# Main processing
if __name__ == "__main__":
import sys
if len(sys.argv) < 4:
print("Usage: blender --background --python script.py input_dir output_dir naming_scheme")
sys.exit(1)
input_dir = sys.argv[-3]
output_dir = sys.argv[-2]
naming_scheme = sys.argv[-1]
print(f"Processing animations from {input_dir} to {output_dir} with {naming_scheme} scheme")
# Create output directory
Path(output_dir).mkdir(parents=True, exist_ok=True)
# Process all .blend files in input directory
blend_files = list(Path(input_dir).glob('*.blend'))
all_results = {
'processed_files': [],
'total_animations': 0,
'total_files': len(blend_files),
'naming_scheme': naming_scheme
}
for blend_file in blend_files:
print(f"Processing: {blend_file}")
result = process_animation_file(str(blend_file), output_dir, naming_scheme)
all_results['processed_files'].append(result)
all_results['total_animations'] += len(result['animations'])
# Save processing report
report_file = Path(output_dir) / 'processing_report.json'
with open(report_file, 'w') as f:
json.dump(all_results, f, indent=2)
print(f"Processing complete. Processed {all_results['total_animations']} animations from {all_results['total_files']} files.")
print(f"Report saved to: {report_file}")
"""
def main():
parser = argparse.ArgumentParser(description='Process Blender animation files')
parser.add_argument('--input-dir', required=True, help='Directory containing .blend files')
parser.add_argument('--output-dir', required=True, help='Directory to export processed animations')
parser.add_argument('--naming-scheme', default='artist', choices=['legacy', 'artist', 'hierarchical', 'semantic'],
help='Target naming scheme for animations')
parser.add_argument('--blender-path', default='blender', help='Path to Blender executable')
parser.add_argument('--dry-run', action='store_true', help='Show what would be processed without actually doing it')
args = parser.parse_args()
# Validate input directory
input_path = Path(args.input_dir)
if not input_path.exists():
print(f"Error: Input directory '{args.input_dir}' does not exist")
sys.exit(1)
# Find .blend files
blend_files = list(input_path.glob('*.blend'))
if not blend_files:
print(f"Warning: No .blend files found in '{args.input_dir}'")
return
print(f"Found {len(blend_files)} .blend files to process:")
for blend_file in blend_files:
print(f"{blend_file.name}")
if args.dry_run:
print(f"\nDry run complete. Would process {len(blend_files)} files with {args.naming_scheme} scheme.")
return
# Create output directory
output_path = Path(args.output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# Create temporary Blender script
script_path = output_path / 'temp_blender_script.py'
with open(script_path, 'w') as f:
f.write(BLENDER_SCRIPT_TEMPLATE)
try:
# Run Blender with the script
print(f"\nProcessing animations with Blender...")
print(f"Input: {args.input_dir}")
print(f"Output: {args.output_dir}")
print(f"Scheme: {args.naming_scheme}")
cmd = [
args.blender_path,
'--background',
'--python', str(script_path),
'--',
args.input_dir,
args.output_dir,
args.naming_scheme
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print("✅ Blender processing completed successfully!")
print(result.stdout)
else:
print("❌ Blender processing failed!")
print("STDOUT:", result.stdout)
print("STDERR:", result.stderr)
sys.exit(1)
# Load and display the processing report
report_file = output_path / 'processing_report.json'
if report_file.exists():
with open(report_file, 'r') as f:
report = json.load(f)
print(f"\n📊 Processing Summary:")
print(f"Files processed: {report['total_files']}")
print(f"Animations exported: {report['total_animations']}")
print(f"Naming scheme: {report['naming_scheme']}")
# Show any errors
errors = []
for file_result in report['processed_files']:
errors.extend(file_result.get('errors', []))
if errors:
print(f"\n⚠️ Errors encountered:")
for error in errors:
print(f"{error}")
finally:
# Clean up temporary script
if script_path.exists():
script_path.unlink()
if __name__ == '__main__':
main()

View File

@ -0,0 +1,361 @@
#!/usr/bin/env node
/**
* Check Naming Conflicts Script
* Analyzes animation names across all schemes to detect potential conflicts
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const PROJECT_ROOT = path.resolve(__dirname, '..')
const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js')
/**
* Check for naming conflicts across schemes
*/
async function checkNamingConflicts () {
try {
console.log('🔍 Checking for animation naming conflicts...')
// Import the AnimationNameMapper
const animationMapperUrl = pathToFileURL(ANIMATION_MAPPER_PATH)
const { AnimationNameMapper } = await import(animationMapperUrl)
const mapper = new AnimationNameMapper()
const conflicts = []
const warnings = []
const statistics = {
totalAnimations: 0,
duplicatesWithinScheme: 0,
crossSchemeConflicts: 0,
ambiguousNames: 0,
validationErrors: 0
}
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
const allAnimationsByScheme = {}
// Collect all animations by scheme
schemes.forEach(scheme => {
allAnimationsByScheme[scheme] = mapper.getAllAnimationsByScheme(scheme)
statistics.totalAnimations += allAnimationsByScheme[scheme].length
})
console.log(`📊 Analyzing ${statistics.totalAnimations} total animations across ${schemes.length} schemes`)
// 1. Check for duplicates within each scheme
schemes.forEach(scheme => {
const animations = allAnimationsByScheme[scheme]
const seen = new Set()
const duplicates = []
animations.forEach(anim => {
if (seen.has(anim)) {
duplicates.push(anim)
statistics.duplicatesWithinScheme++
}
seen.add(anim)
})
if (duplicates.length > 0) {
conflicts.push({
type: 'duplicate_within_scheme',
scheme,
animations: duplicates,
severity: 'error',
message: `Duplicate animations found within ${scheme} scheme`
})
}
})
// 2. Check for cross-scheme conflicts (same name in different schemes with different meanings)
const nameToSchemes = {}
schemes.forEach(scheme => {
allAnimationsByScheme[scheme].forEach(anim => {
if (!nameToSchemes[anim]) {
nameToSchemes[anim] = []
}
nameToSchemes[anim].push(scheme)
})
})
Object.entries(nameToSchemes).forEach(([animName, animSchemes]) => {
if (animSchemes.length > 1) {
// Check if they map to the same semantic meaning
try {
const allSemantic = animSchemes.map(scheme => {
try {
return mapper.convert(animName, 'semantic')
} catch {
return null
}
}).filter(Boolean)
const uniqueSemantic = [...new Set(allSemantic)]
if (uniqueSemantic.length > 1) {
conflicts.push({
type: 'cross_scheme_conflict',
animationName: animName,
schemes: animSchemes,
semanticMappings: uniqueSemantic,
severity: 'error',
message: `Animation "${animName}" exists in multiple schemes but maps to different meanings`
})
statistics.crossSchemeConflicts++
}
} catch (error) {
warnings.push({
type: 'conversion_error',
animationName: animName,
schemes: animSchemes,
error: error.message,
severity: 'warning'
})
}
}
})
// 3. Check for ambiguous names (could be interpreted as multiple schemes)
const allAnimations = Object.values(allAnimationsByScheme).flat()
const uniqueAnimations = [...new Set(allAnimations)]
uniqueAnimations.forEach(anim => {
const detectedScheme = mapper.detectScheme(anim)
let possibleSchemes = 0
// Test if name could belong to other schemes
schemes.forEach(scheme => {
try {
const converted = mapper.convert(anim, scheme)
if (converted) possibleSchemes++
} catch {
// Can't convert to this scheme
}
})
if (possibleSchemes > 2) {
warnings.push({
type: 'ambiguous_name',
animationName: anim,
detectedScheme,
possibleSchemes,
severity: 'warning',
message: `Animation "${anim}" could be interpreted as belonging to multiple schemes`
})
statistics.ambiguousNames++
}
})
// 4. Validate all animations can be properly converted
uniqueAnimations.forEach(anim => {
schemes.forEach(targetScheme => {
try {
mapper.convert(anim, targetScheme)
} catch (error) {
if (!error.message.includes('not found in mapping')) {
warnings.push({
type: 'validation_error',
animationName: anim,
targetScheme,
error: error.message,
severity: 'warning'
})
statistics.validationErrors++
}
}
})
})
// 5. Check for naming convention violations
const conventionViolations = []
// Legacy should follow pattern: word_word_L/S
allAnimationsByScheme.legacy.forEach(anim => {
if (!/^[a-z]+(_[a-z]+)*_[LS]$/.test(anim)) {
conventionViolations.push({
type: 'convention_violation',
scheme: 'legacy',
animationName: anim,
expectedPattern: 'word_word_L/S',
severity: 'warning'
})
}
})
// Artist should follow pattern: Owen_PascalCase
allAnimationsByScheme.artist.forEach(anim => {
if (!/^Owen_[A-Z][a-zA-Z]*$/.test(anim)) {
conventionViolations.push({
type: 'convention_violation',
scheme: 'artist',
animationName: anim,
expectedPattern: 'Owen_PascalCase',
severity: 'warning'
})
}
})
// Hierarchical should follow pattern: owen.category.subcategory
allAnimationsByScheme.hierarchical.forEach(anim => {
if (!/^owen(\.[a-z]+)+$/.test(anim)) {
conventionViolations.push({
type: 'convention_violation',
scheme: 'hierarchical',
animationName: anim,
expectedPattern: 'owen.category.subcategory',
severity: 'warning'
})
}
})
// Semantic should follow pattern: OwenPascalCase
allAnimationsByScheme.semantic.forEach(anim => {
if (!/^Owen[A-Z][a-zA-Z]*$/.test(anim)) {
conventionViolations.push({
type: 'convention_violation',
scheme: 'semantic',
animationName: anim,
expectedPattern: 'OwenPascalCase',
severity: 'warning'
})
}
})
warnings.push(...conventionViolations)
// Generate report
const report = {
timestamp: new Date().toISOString(),
summary: {
status: conflicts.length === 0 ? 'PASS' : 'FAIL',
totalConflicts: conflicts.length,
totalWarnings: warnings.length,
statistics
},
conflicts,
warnings,
recommendations: generateRecommendations(conflicts, warnings),
schemes: Object.fromEntries(
schemes.map(scheme => [
scheme,
{
animationCount: allAnimationsByScheme[scheme].length,
sampleAnimations: allAnimationsByScheme[scheme].slice(0, 3)
}
])
)
}
// Save report
const reportsDir = path.join(PROJECT_ROOT, 'reports')
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true })
}
fs.writeFileSync(
path.join(reportsDir, 'naming-conflicts.json'),
JSON.stringify(report, null, 2),
'utf8'
)
// Print summary
console.log('\n📋 NAMING CONFLICT ANALYSIS SUMMARY')
console.log('='.repeat(50))
console.log(`Status: ${report.summary.status}`)
console.log(`Total Conflicts: ${conflicts.length}`)
console.log(`Total Warnings: ${warnings.length}`)
console.log(`Animations Analyzed: ${statistics.totalAnimations}`)
if (conflicts.length > 0) {
console.log('\n❌ CONFLICTS:')
conflicts.forEach((conflict, i) => {
console.log(`${i + 1}. ${conflict.type}: ${conflict.message}`)
})
}
if (warnings.length > 0 && warnings.length <= 10) {
console.log('\n⚠ WARNINGS:')
warnings.slice(0, 10).forEach((warning, i) => {
console.log(`${i + 1}. ${warning.type}: ${warning.message || warning.animationName}`)
})
if (warnings.length > 10) {
console.log(`... and ${warnings.length - 10} more warnings`)
}
}
console.log(`\n📁 Full report saved to: ${path.join(reportsDir, 'naming-conflicts.json')}`)
// Exit with error code if conflicts found
if (conflicts.length > 0) {
process.exit(1)
}
return report
} catch (error) {
console.error('❌ Error checking naming conflicts:', error.message)
process.exit(1)
}
}
/**
* Generate recommendations based on conflicts and warnings
*/
function generateRecommendations (conflicts, warnings) {
const recommendations = []
if (conflicts.some(c => c.type === 'duplicate_within_scheme')) {
recommendations.push({
type: 'fix_duplicates',
priority: 'high',
action: 'Remove duplicate animation names within each scheme',
description: 'Duplicate names can cause unpredictable behavior'
})
}
if (conflicts.some(c => c.type === 'cross_scheme_conflict')) {
recommendations.push({
type: 'resolve_cross_scheme',
priority: 'high',
action: 'Ensure names in different schemes map to the same semantic meaning',
description: 'Cross-scheme conflicts break the mapping system'
})
}
const conventionViolations = warnings.filter(w => w.type === 'convention_violation')
if (conventionViolations.length > 0) {
recommendations.push({
type: 'fix_conventions',
priority: 'medium',
action: `Fix ${conventionViolations.length} naming convention violations`,
description: 'Consistent naming conventions improve maintainability'
})
}
if (warnings.some(w => w.type === 'ambiguous_name')) {
recommendations.push({
type: 'clarify_ambiguous',
priority: 'low',
action: 'Review ambiguous animation names for clarity',
description: 'Ambiguous names can be confusing for developers'
})
}
return recommendations
}
// Run the script if called directly
if (process.argv[1] === __filename) {
checkNamingConflicts()
.then(report => {
console.log('✅ Naming conflict check complete!')
})
.catch(error => {
console.error('💥 Script failed:', error)
process.exit(1)
})
}

View File

@ -0,0 +1,433 @@
#!/usr/bin/env node
/**
* Convert Animation Names Script
* Converts animation names between different schemes
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const PROJECT_ROOT = path.resolve(__dirname, '..')
const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js')
/**
* Convert animation names based on command line arguments or file input
*/
async function convertAnimationNames () {
try {
const args = process.argv.slice(2)
// Show help if no arguments provided
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
showHelp()
return
}
console.log('🔄 Converting Animation Names...')
// Import the AnimationNameMapper
const { AnimationNameMapper } = await import(ANIMATION_MAPPER_PATH)
const mapper = new AnimationNameMapper()
const options = parseArguments(args)
if (options.inputFile) {
await convertFromFile(mapper, options)
} else if (options.animationName) {
await convertSingle(mapper, options)
} else if (options.batchConvert) {
await convertBatch(mapper, options)
} else {
console.error('❌ No input provided. Use --help for usage information.')
process.exit(1)
}
} catch (error) {
console.error('❌ Error converting animation names:', error.message)
process.exit(1)
}
}
/**
* Parse command line arguments
*/
function parseArguments (args) {
const options = {
animationName: null,
fromScheme: null,
toScheme: null,
inputFile: null,
outputFile: null,
batchConvert: false,
allSchemes: false,
validate: false
}
for (let i = 0; i < args.length; i++) {
const arg = args[i]
const nextArg = args[i + 1]
switch (arg) {
case '--name':
case '-n':
options.animationName = nextArg
i++
break
case '--from':
case '-f':
options.fromScheme = nextArg
i++
break
case '--to':
case '-t':
options.toScheme = nextArg
i++
break
case '--input':
case '-i':
options.inputFile = nextArg
i++
break
case '--output':
case '-o':
options.outputFile = nextArg
i++
break
case '--batch':
case '-b':
options.batchConvert = true
break
case '--all-schemes':
case '-a':
options.allSchemes = true
break
case '--validate':
case '-v':
options.validate = true
break
}
}
return options
}
/**
* Convert a single animation name
*/
async function convertSingle (mapper, options) {
const { animationName, fromScheme, toScheme, allSchemes, validate } = options
console.log(`\n🎯 Converting: ${animationName}`)
if (validate) {
const validation = mapper.validateAnimationName(animationName)
console.log('\n✅ Validation Result:')
console.log(`Valid: ${validation.isValid}`)
if (validation.detectedScheme) {
console.log(`Detected Scheme: ${validation.detectedScheme}`)
}
if (validation.suggestions?.length > 0) {
console.log(`Suggestions: ${validation.suggestions.join(', ')}`)
}
if (validation.errors?.length > 0) {
console.log(`Errors: ${validation.errors.join(', ')}`)
}
}
if (allSchemes) {
// Convert to all schemes
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
console.log('\n🔄 Converting to all schemes:')
schemes.forEach(scheme => {
try {
const converted = mapper.convert(animationName, scheme)
console.log(`${scheme.padEnd(12)}: ${converted}`)
} catch (error) {
console.log(`${scheme.padEnd(12)}: ❌ ${error.message}`)
}
})
} else if (toScheme) {
// Convert to specific scheme
try {
const converted = mapper.convert(animationName, toScheme)
console.log(`\n🎯 Result: ${converted}`)
if (fromScheme) {
console.log(`From: ${fromScheme} -> To: ${toScheme}`)
} else {
const detectedScheme = mapper.detectScheme(animationName)
console.log(`From: ${detectedScheme} -> To: ${toScheme}`)
}
} catch (error) {
console.error(`❌ Conversion failed: ${error.message}`)
process.exit(1)
}
} else {
console.log(' No target scheme specified. Use --to <scheme> or --all-schemes')
}
}
/**
* Convert animation names from a file
*/
async function convertFromFile (mapper, options) {
const { inputFile, outputFile, toScheme, allSchemes } = options
if (!fs.existsSync(inputFile)) {
throw new Error(`Input file not found: ${inputFile}`)
}
console.log(`📁 Reading from file: ${inputFile}`)
const content = fs.readFileSync(inputFile, 'utf8')
let animationNames = []
try {
// Try to parse as JSON first
const parsed = JSON.parse(content)
if (Array.isArray(parsed)) {
animationNames = parsed
} else if (parsed.animations && Array.isArray(parsed.animations)) {
animationNames = parsed.animations
} else {
animationNames = Object.values(parsed).filter(v => typeof v === 'string')
}
} catch {
// If not JSON, treat as line-separated text
animationNames = content.split('\n')
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'))
}
console.log(`📋 Found ${animationNames.length} animation names to convert`)
const results = {
timestamp: new Date().toISOString(),
inputFile,
totalAnimations: animationNames.length,
conversions: [],
errors: []
}
if (allSchemes) {
// Convert each animation to all schemes
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
animationNames.forEach(animName => {
const animResult = {
original: animName,
conversions: {}
}
schemes.forEach(scheme => {
try {
animResult.conversions[scheme] = mapper.convert(animName, scheme)
} catch (error) {
animResult.conversions[scheme] = { error: error.message }
results.errors.push(`${animName} -> ${scheme}: ${error.message}`)
}
})
results.conversions.push(animResult)
})
} else if (toScheme) {
// Convert to specific scheme
animationNames.forEach(animName => {
const animResult = {
original: animName,
target: toScheme
}
try {
animResult.converted = mapper.convert(animName, toScheme)
animResult.fromScheme = mapper.detectScheme(animName)
} catch (error) {
animResult.error = error.message
results.errors.push(`${animName}: ${error.message}`)
}
results.conversions.push(animResult)
})
}
// Save results
if (outputFile) {
fs.writeFileSync(outputFile, JSON.stringify(results, null, 2), 'utf8')
console.log(`📄 Results saved to: ${outputFile}`)
} else {
// Print to console
console.log('\n📊 Conversion Results:')
results.conversions.forEach((result, index) => {
console.log(`\n${index + 1}. ${result.original}`)
if (result.conversions) {
Object.entries(result.conversions).forEach(([scheme, value]) => {
if (typeof value === 'string') {
console.log(` ${scheme}: ${value}`)
} else {
console.log(` ${scheme}: ❌ ${value.error}`)
}
})
} else if (result.converted) {
console.log(` ${result.target}: ${result.converted}`)
} else if (result.error) {
console.log(`${result.error}`)
}
})
}
if (results.errors.length > 0) {
console.log(`\n⚠️ ${results.errors.length} errors encountered`)
if (results.errors.length <= 5) {
results.errors.forEach(error => console.log(`${error}`))
}
}
console.log(`\n✅ Processed ${results.totalAnimations} animations`)
}
/**
* Batch convert all animations in the current mapper
*/
async function convertBatch (mapper, options) {
const { outputFile } = options
console.log('🔄 Batch converting all animations in the system...')
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
const allResults = {
timestamp: new Date().toISOString(),
totalAnimations: 0,
schemeStats: {},
conversionMatrix: {}
}
schemes.forEach(fromScheme => {
const animations = mapper.getAllAnimationsByScheme(fromScheme)
allResults.schemeStats[fromScheme] = animations.length
allResults.totalAnimations += animations.length
if (!allResults.conversionMatrix[fromScheme]) {
allResults.conversionMatrix[fromScheme] = {}
}
schemes.forEach(targetScheme => {
const conversions = []
let errors = 0
animations.forEach(anim => {
try {
const converted = mapper.convert(anim, targetScheme)
conversions.push({ original: anim, converted })
} catch (error) {
conversions.push({ original: anim, error: error.message })
errors++
}
})
allResults.conversionMatrix[fromScheme][targetScheme] = {
total: animations.length,
successful: animations.length - errors,
errors,
successRate: Math.round(((animations.length - errors) / animations.length) * 100),
conversions: conversions.slice(0, 10) // Include sample conversions
}
})
})
// Print summary
console.log('\n📊 Batch Conversion Summary:')
console.log(`Total animations: ${allResults.totalAnimations}`)
console.log('\nBy scheme:')
Object.entries(allResults.schemeStats).forEach(([scheme, count]) => {
console.log(` ${scheme}: ${count} animations`)
})
console.log('\nConversion matrix (success rates):')
schemes.forEach(fromScheme => {
console.log(`\n${fromScheme}:`)
schemes.forEach(toScheme => {
const result = allResults.conversionMatrix[fromScheme][toScheme]
console.log(` -> ${toScheme}: ${result.successRate}% (${result.successful}/${result.total})`)
})
})
// Save results
if (outputFile) {
fs.writeFileSync(outputFile, JSON.stringify(allResults, null, 2), 'utf8')
console.log(`\n📄 Full results saved to: ${outputFile}`)
} else {
// Save to default location
const reportsDir = path.join(PROJECT_ROOT, 'reports')
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true })
}
const defaultFile = path.join(reportsDir, 'batch-conversion-results.json')
fs.writeFileSync(defaultFile, JSON.stringify(allResults, null, 2), 'utf8')
console.log(`\n📄 Results saved to: ${defaultFile}`)
}
}
/**
* Show help information
*/
function showHelp () {
console.log(`
🎬 Animation Name Converter
Convert animation names between different naming schemes in the Owen Animation System.
USAGE:
node convert-animation-names.js [OPTIONS]
SINGLE CONVERSION:
--name, -n <name> Animation name to convert
--to, -t <scheme> Target scheme (legacy|artist|hierarchical|semantic)
--all-schemes, -a Convert to all schemes
--validate, -v Validate the animation name
FILE CONVERSION:
--input, -i <file> Input file with animation names (JSON or line-separated)
--output, -o <file> Output file for results (optional)
--to, -t <scheme> Target scheme for conversion
BATCH OPERATIONS:
--batch, -b Convert all animations in the system
--output, -o <file> Output file for batch results
EXAMPLES:
# Convert single animation to semantic scheme
node convert-animation-names.js --name wait_idle_L --to semantic
# Convert to all schemes
node convert-animation-names.js --name Owen_ReactAngry --all-schemes
# Validate an animation name
node convert-animation-names.js --name unknown_animation --validate
# Convert from file
node convert-animation-names.js --input animations.json --to artist --output results.json
# Batch convert all animations
node convert-animation-names.js --batch --output full-conversion-matrix.json
SCHEMES:
legacy - e.g., wait_idle_L, react_angry_S
artist - e.g., Owen_WaitIdle, Owen_ReactAngry
hierarchical - e.g., owen.state.wait.idle.loop
semantic - e.g., OwenWaitIdleLoop, OwenReactAngryShort
`)
}
// Run the script if called directly
if (process.argv[1] === __filename) {
convertAnimationNames()
.catch(error => {
console.error('💥 Script failed:', error)
process.exit(1)
})
}

View File

@ -0,0 +1,252 @@
#!/usr/bin/env node
/**
* Generate Animation Constants Script
* Automatically generates/updates AnimationConstants.js based on current AnimationNameMapper definitions
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const PROJECT_ROOT = path.resolve(__dirname, '..')
const ANIMATION_CONSTANTS_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationConstants.js')
const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js')
/**
* Generate animation constants file content
*/
async function generateAnimationConstants () {
try {
console.log('🔧 Generating Animation Constants...')
// Import the AnimationNameMapper to get current definitions
const animationMapperUrl = pathToFileURL(ANIMATION_MAPPER_PATH)
const { AnimationNameMapper } = await import(animationMapperUrl)
const mapper = new AnimationNameMapper()
// Get all animation names by scheme
const legacyAnimations = mapper.getAllAnimationsByScheme('legacy')
const artistAnimations = mapper.getAllAnimationsByScheme('artist')
const hierarchicalAnimations = mapper.getAllAnimationsByScheme('hierarchical')
const semanticAnimations = mapper.getAllAnimationsByScheme('semantic')
const timestamp = new Date().toISOString()
const constantsContent = `/**
* Animation Constants - Auto-generated file
*
* This file contains type-safe constants for all animation naming schemes
* supported by the Owen Animation System.
*
* Generated: ${timestamp}
*
* @fileoverview Auto-generated animation constants for all naming schemes
* @module AnimationConstants
*/
// Import the core mapper for utility functions
import { AnimationNameMapper } from './AnimationNameMapper.js'
/**
* Naming scheme enumeration
* @readonly
* @enum {string}
*/
export const NamingSchemes = Object.freeze({
LEGACY: 'legacy',
ARTIST: 'artist',
HIERARCHICAL: 'hierarchical',
SEMANTIC: 'semantic'
})
/**
* Legacy animation names (e.g., wait_idle_L)
* @readonly
*/
export const LegacyAnimations = Object.freeze({
${legacyAnimations.map(anim => {
const constantName = anim.toUpperCase().replace(/[^A-Z0-9]/g, '_')
return ` ${constantName}: '${anim}'`
}).join(',\n')}
})
/**
* Artist-friendly animation names (e.g., Owen_WaitIdle)
* @readonly
*/
export const ArtistAnimations = Object.freeze({
${artistAnimations.map(anim => {
const constantName = anim.replace(/^Owen_/, '').toUpperCase().replace(/[^A-Z0-9]/g, '_')
return ` ${constantName}: '${anim}'`
}).join(',\n')}
})
/**
* Hierarchical animation names (e.g., owen.state.wait.idle.loop)
* @readonly
*/
export const HierarchicalAnimations = Object.freeze({
${hierarchicalAnimations.map(anim => {
const constantName = anim.replace(/owen\./, '').split('.').map(part =>
part.charAt(0).toUpperCase() + part.slice(1)
).join('_').toUpperCase()
return ` ${constantName}: '${anim}'`
}).join(',\n')}
})
/**
* Semantic animation names (e.g., OwenWaitIdleLoop)
* @readonly
*/
export const SemanticAnimations = Object.freeze({
${semanticAnimations.map(anim => {
const constantName = anim.replace(/^Owen/, '').replace(/([A-Z])/g, '_$1').toUpperCase().substring(1)
return ` ${constantName}: '${anim}'`
}).join(',\n')}
})
/**
* All animation constants grouped by scheme
* @readonly
*/
export const AnimationsByScheme = Object.freeze({
[NamingSchemes.LEGACY]: LegacyAnimations,
[NamingSchemes.ARTIST]: ArtistAnimations,
[NamingSchemes.HIERARCHICAL]: HierarchicalAnimations,
[NamingSchemes.SEMANTIC]: SemanticAnimations
})
// Create global mapper instance for utility functions
const mapper = new AnimationNameMapper()
/**
* Convert an animation name between schemes
* @param {string} animationName - The animation name to convert
* @param {string} targetScheme - The target naming scheme
* @returns {string} The converted animation name
* @throws {Error} If conversion fails
*/
export function convertAnimationName(animationName, targetScheme) {
return mapper.convert(animationName, targetScheme)
}
/**
* Get all animation names for a specific scheme
* @param {string} scheme - The naming scheme
* @returns {string[]} Array of animation names
*/
export function getAllAnimationNames(scheme) {
return mapper.getAllAnimationsByScheme(scheme)
}
/**
* Validate an animation name and get suggestions
* @param {string} animationName - The animation name to validate
* @returns {Object} Validation result with isValid flag and suggestions
*/
export function validateAnimationName(animationName) {
return mapper.validateAnimationName(animationName)
}
/**
* Get animations filtered by state and emotion
* @param {string} state - The animation state (wait, react, sleep, etc.)
* @param {string} [emotion] - Optional emotion filter (angry, happy, etc.)
* @param {string} [scheme='semantic'] - The naming scheme to use
* @returns {string[]} Array of matching animation names
*/
export function getAnimationsByStateAndEmotion(state, emotion = null, scheme = 'semantic') {
const allAnimations = getAllAnimationNames(scheme)
return allAnimations.filter(anim => {
const lowerAnim = anim.toLowerCase()
const hasState = lowerAnim.includes(state.toLowerCase())
const hasEmotion = !emotion || lowerAnim.includes(emotion.toLowerCase())
return hasState && hasEmotion
})
}
/**
* Animation metadata for development tools
* @readonly
*/
export const AnimationMetadata = Object.freeze({
totalAnimations: ${legacyAnimations.length},
schemes: Object.keys(NamingSchemes).length,
generatedAt: '${timestamp}',
version: '1.0.0'
})
// Default export for convenience
export default {
NamingSchemes,
LegacyAnimations,
ArtistAnimations,
HierarchicalAnimations,
SemanticAnimations,
AnimationsByScheme,
convertAnimationName,
getAllAnimationNames,
validateAnimationName,
getAnimationsByStateAndEmotion,
AnimationMetadata
}
`
// Write the generated constants file
fs.writeFileSync(ANIMATION_CONSTANTS_PATH, constantsContent, 'utf8')
console.log('✅ Animation Constants generated successfully!')
console.log(`📝 Generated ${legacyAnimations.length} animation constants across 4 schemes`)
console.log(`📍 File: ${ANIMATION_CONSTANTS_PATH}`)
// Generate summary report
const report = {
generated: timestamp,
totalAnimations: legacyAnimations.length,
schemes: {
legacy: legacyAnimations.length,
artist: artistAnimations.length,
hierarchical: hierarchicalAnimations.length,
semantic: semanticAnimations.length
},
outputFile: ANIMATION_CONSTANTS_PATH
}
// Ensure reports directory exists
const reportsDir = path.join(PROJECT_ROOT, 'reports')
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true })
}
// Write report
fs.writeFileSync(
path.join(reportsDir, 'animation-constants-generation.json'),
JSON.stringify(report, null, 2),
'utf8'
)
return report
} catch (error) {
console.error('❌ Error generating Animation Constants:', error.message)
process.exit(1)
}
}
// Run the script if called directly
if (process.argv[1] === __filename) {
generateAnimationConstants()
.then(report => {
console.log('📊 Generation complete!')
console.log(JSON.stringify(report, null, 2))
})
.catch(error => {
console.error('💥 Script failed:', error)
process.exit(1)
})
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,802 @@
#!/usr/bin/env node
/**
* Generate Scheme Examples Script
* Creates example files for each naming scheme
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const PROJECT_ROOT = path.resolve(__dirname, '..')
const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js')
/**
* Generate scheme examples
*/
async function generateSchemeExamples () {
try {
console.log('🎨 Generating Scheme Examples...')
// Import the AnimationNameMapper
const { AnimationNameMapper } = await import(ANIMATION_MAPPER_PATH)
const mapper = new AnimationNameMapper()
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
const examplesDir = path.join(PROJECT_ROOT, 'examples', 'scheme-examples')
// Create examples directory
if (!fs.existsSync(examplesDir)) {
fs.mkdirSync(examplesDir, { recursive: true })
}
// Generate examples for each scheme
for (const scheme of schemes) {
await generateSchemeExample(mapper, scheme, examplesDir)
}
// Generate comparison example
await generateComparisonExample(mapper, examplesDir)
// Generate integration examples
await generateIntegrationExamples(mapper, examplesDir)
console.log('✅ Scheme examples generated successfully!')
} catch (error) {
console.error('❌ Error generating scheme examples:', error.message)
process.exit(1)
}
}
/**
* Generate example for a specific scheme
*/
async function generateSchemeExample (mapper, scheme, examplesDir) {
const animations = mapper.getAllAnimationsByScheme(scheme)
const schemeTitle = scheme.charAt(0).toUpperCase() + scheme.slice(1)
const content = `# ${schemeTitle} Scheme Examples
This example demonstrates using animations with the **${scheme}** naming scheme.
## Animation Names (${animations.length} total)
${animations.map(name => `- \`${name}\``).join('\n')}
## Usage Example
\`\`\`javascript
import { OwenAnimationContext } from '@kjanat/owen'
const context = new OwenAnimationContext(gltf)
// Load ${scheme} scheme animations
${animations.slice(0, 5).map(name => `const clip${animations.indexOf(name) + 1} = context.getClip('${name}')`).join('\n')}
// Play the first animation
clip1.play()
\`\`\`
## Scheme Characteristics
${getSchemeCharacteristics(scheme)}
## Converting to Other Schemes
\`\`\`javascript
import { AnimationNameMapper } from '@kjanat/owen'
const mapper = new AnimationNameMapper()
// Convert ${scheme} animations to other schemes
${animations.slice(0, 3).map(name => {
const otherSchemes = ['legacy', 'artist', 'hierarchical', 'semantic'].filter(s => s !== scheme)
return `
// ${name}
${otherSchemes.map(targetScheme => {
try {
const converted = mapper.convert(name, targetScheme)
return `const ${targetScheme} = mapper.convert('${name}', '${targetScheme}') // '${converted}'`
} catch {
return `// Cannot convert to ${targetScheme}`
}
}).join('\n')}`
}).join('\n')}
\`\`\`
## Best Practices for ${schemeTitle} Scheme
${getSchemeBestPractices(scheme)}
`
fs.writeFileSync(path.join(examplesDir, `${scheme}-example.md`), content, 'utf8')
console.log(`📄 Generated: ${scheme}-example.md`)
}
/**
* Generate comparison example
*/
async function generateComparisonExample (mapper, examplesDir) {
const sampleAnimations = ['wait_idle_L', 'react_angry_S', 'sleep_peace_L', 'type_idle_L']
const content = `# Multi-Scheme Comparison Example
This example shows the same animations represented in all four naming schemes.
## Side-by-Side Comparison
| Animation | Legacy | Artist | Hierarchical | Semantic |
|-----------|---------|--------|--------------|----------|
${sampleAnimations.map(legacyName => {
try {
const artist = mapper.convert(legacyName, 'artist')
const hierarchical = mapper.convert(legacyName, 'hierarchical')
const semantic = mapper.convert(legacyName, 'semantic')
return `| Base | \`${legacyName}\` | \`${artist}\` | \`${hierarchical}\` | \`${semantic}\` |`
} catch {
return `| Base | \`${legacyName}\` | - | - | - |`
}
}).join('\n')}
## Loading the Same Animation in Different Schemes
\`\`\`javascript
import { OwenAnimationContext, AnimationNameMapper } from '@kjanat/owen'
const context = new OwenAnimationContext(gltf)
const mapper = new AnimationNameMapper()
// These all load the same animation clip
const clip1 = context.getClip('wait_idle_L') // Legacy
const clip2 = context.getClip('Owen_WaitIdle') // Artist
const clip3 = context.getClip('owen.state.wait.idle.loop') // Hierarchical
const clip4 = context.getClip('OwenWaitIdleLoop') // Semantic
// All clips are identical
console.log(clip1 === clip2 === clip3 === clip4) // true
\`\`\`
## Dynamic Scheme Conversion
\`\`\`javascript
function loadAnimationInScheme(animationName, targetScheme) {
const mapper = new AnimationNameMapper()
try {
// Convert to target scheme
const convertedName = mapper.convert(animationName, targetScheme)
console.log(\`Converted \${animationName} to \${convertedName}\`)
// Load the animation
return context.getClip(convertedName)
} catch (error) {
console.error('Conversion failed:', error.message)
return null
}
}
// Usage examples
const semanticClip = loadAnimationInScheme('wait_idle_L', 'semantic')
const artistClip = loadAnimationInScheme('OwenReactAngryShort', 'artist')
const hierarchicalClip = loadAnimationInScheme('Owen_SleepPeace', 'hierarchical')
\`\`\`
## Scheme Detection and Auto-Conversion
\`\`\`javascript
function smartLoadAnimation(animationName, preferredScheme = 'semantic') {
const mapper = new AnimationNameMapper()
// Detect the current scheme
const currentScheme = mapper.detectScheme(animationName)
console.log(\`Detected scheme: \${currentScheme}\`)
if (currentScheme === preferredScheme) {
// Already in preferred scheme
return context.getClip(animationName)
} else {
// Convert to preferred scheme
const converted = mapper.convert(animationName, preferredScheme)
console.log(\`Converted to \${preferredScheme}: \${converted}\`)
return context.getClip(converted)
}
}
// Examples
smartLoadAnimation('wait_idle_L') // Converts to semantic
smartLoadAnimation('Owen_ReactAngry') // Converts to semantic
smartLoadAnimation('OwenSleepPeaceLoop') // Already semantic, no conversion
\`\`\`
## Validation Across Schemes
\`\`\`javascript
function validateAndConvert(animationName) {
const mapper = new AnimationNameMapper()
const validation = mapper.validateAnimationName(animationName)
if (validation.isValid) {
console.log(\`✅ Valid animation: \${animationName}\`)
console.log(\`Detected scheme: \${validation.detectedScheme}\`)
// Show all scheme variants
const allNames = mapper.getAllNames(animationName)
console.log('All scheme variants:', allNames)
return allNames
} else {
console.log(\`❌ Invalid animation: \${animationName}\`)
console.log('Suggestions:', validation.suggestions.slice(0, 3))
return null
}
}
// Test with various inputs
validateAndConvert('wait_idle_L') // Valid
validateAndConvert('Owen_Unknown') // Invalid, shows suggestions
validateAndConvert('typing_fast_L') // Valid if exists
\`\`\`
## Use Case Examples
### For Artists (Blender Workflow)
\`\`\`javascript
// Artists work with Owen_AnimationName format
const artistAnimations = [
'Owen_WaitIdle',
'Owen_ReactAngry',
'Owen_SleepPeace',
'Owen_TypeFast'
]
// Automatically convert to code-friendly names
const codeAnimations = artistAnimations.map(anim =>
mapper.convert(anim, 'semantic')
)
console.log(codeAnimations)
// ['OwenWaitIdleLoop', 'OwenReactAngryShort', 'OwenSleepPeaceLoop', 'OwenTypeFastLoop']
\`\`\`
### For Developers (Type Safety)
\`\`\`javascript
import { SemanticAnimations, AnimationsByScheme } from '@kjanat/owen'
// Type-safe animation loading
context.getClip(SemanticAnimations.WAIT_IDLE_LOOP)
context.getClip(SemanticAnimations.REACT_ANGRY_SHORT)
// Scheme-specific constants
const legacyAnimations = AnimationsByScheme.legacy
const artistAnimations = AnimationsByScheme.artist
\`\`\`
### For Large Projects (Organization)
\`\`\`javascript
// Use hierarchical scheme for better organization
const waitAnimations = mapper.getAllAnimationsByScheme('hierarchical')
.filter(anim => anim.includes('.wait.'))
const reactAnimations = mapper.getAllAnimationsByScheme('hierarchical')
.filter(anim => anim.includes('.react.'))
console.log('Wait animations:', waitAnimations)
console.log('React animations:', reactAnimations)
\`\`\`
This example demonstrates the flexibility and power of the multi-scheme animation system!
`
fs.writeFileSync(path.join(examplesDir, 'comparison-example.md'), content, 'utf8')
console.log('📄 Generated: comparison-example.md')
}
/**
* Generate integration examples
*/
async function generateIntegrationExamples (mapper, examplesDir) {
const content = `# Integration Examples
Real-world integration examples for different frameworks and use cases.
## React Integration
\`\`\`jsx
import React, { useState, useEffect } from 'react'
import { OwenAnimationContext, AnimationNameMapper } from '@kjanat/owen'
function AnimationPlayer({ gltf }) {
const [context] = useState(() => new OwenAnimationContext(gltf))
const [mapper] = useState(() => new AnimationNameMapper())
const [currentAnimation, setCurrentAnimation] = useState('OwenWaitIdleLoop')
const [scheme, setScheme] = useState('semantic')
const availableAnimations = mapper.getAllAnimationsByScheme(scheme)
const playAnimation = (animationName) => {
try {
const clip = context.getClip(animationName)
clip.play()
setCurrentAnimation(animationName)
} catch (error) {
console.error('Failed to play animation:', error)
}
}
return (
<div>
<h3>Animation Player</h3>
<select value={scheme} onChange={(e) => setScheme(e.target.value)}>
<option value="legacy">Legacy</option>
<option value="artist">Artist</option>
<option value="hierarchical">Hierarchical</option>
<option value="semantic">Semantic</option>
</select>
<div>
<h4>Available Animations ({scheme} scheme)</h4>
{availableAnimations.map(anim => (
<button
key={anim}
onClick={() => playAnimation(anim)}
disabled={anim === currentAnimation}
>
{anim}
</button>
))}
</div>
<p>Currently playing: {currentAnimation}</p>
</div>
)
}
\`\`\`
## Vue.js Integration
\`\`\`vue
<template>
<div class="animation-controller">
<h3>Animation Controller</h3>
<div class="scheme-selector">
<label>Naming Scheme:</label>
<select v-model="selectedScheme">
<option value="legacy">Legacy</option>
<option value="artist">Artist</option>
<option value="hierarchical">Hierarchical</option>
<option value="semantic">Semantic</option>
</select>
</div>
<div class="animation-grid">
<div
v-for="animation in availableAnimations"
:key="animation"
class="animation-card"
:class="{ active: animation === currentAnimation }"
@click="playAnimation(animation)"
>
{{ animation }}
</div>
</div>
<div class="conversion-display" v-if="currentAnimation">
<h4>Current Animation in All Schemes:</h4>
<div v-for="(name, scheme) in allSchemeNames" :key="scheme">
<strong>{{ scheme }}:</strong> {{ name }}
</div>
</div>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue'
import { OwenAnimationContext, AnimationNameMapper } from '@kjanat/owen'
export default {
props: ['gltf'],
setup(props) {
const context = new OwenAnimationContext(props.gltf)
const mapper = new AnimationNameMapper()
const selectedScheme = ref('semantic')
const currentAnimation = ref('')
const availableAnimations = computed(() =>
mapper.getAllAnimationsByScheme(selectedScheme.value)
)
const allSchemeNames = computed(() => {
if (!currentAnimation.value) return {}
try {
return mapper.getAllNames(currentAnimation.value)
} catch {
return {}
}
})
const playAnimation = (animationName) => {
try {
const clip = context.getClip(animationName)
clip.play()
currentAnimation.value = animationName
} catch (error) {
console.error('Failed to play animation:', error)
}
}
// Set default animation when scheme changes
watch(selectedScheme, (newScheme) => {
const animations = mapper.getAllAnimationsByScheme(newScheme)
if (animations.length > 0) {
playAnimation(animations[0])
}
}, { immediate: true })
return {
selectedScheme,
currentAnimation,
availableAnimations,
allSchemeNames,
playAnimation
}
}
}
</script>
\`\`\`
## Node.js Build Script Integration
\`\`\`javascript
// build-animations.js
import fs from 'fs'
import path from 'path'
import { AnimationNameMapper } from '@kjanat/owen'
class AnimationBuildProcessor {
constructor() {
this.mapper = new AnimationNameMapper()
}
async processBlenderAssets(inputDir, outputDir) {
console.log('Processing Blender animation assets...')
const blenderFiles = fs.readdirSync(inputDir)
.filter(file => file.endsWith('.blend'))
const processedAssets = []
for (const blenderFile of blenderFiles) {
const baseName = path.basename(blenderFile, '.blend')
// Convert artist naming to semantic for code
try {
const semanticName = this.mapper.convert(baseName, 'semantic')
processedAssets.push({
blenderFile: baseName,
semanticName,
artistName: baseName,
legacyName: this.mapper.convert(semanticName, 'legacy'),
hierarchicalName: this.mapper.convert(semanticName, 'hierarchical')
})
console.log(\`Processed: \${baseName} -> \${semanticName}\`)
} catch (error) {
console.warn(\`Skipped invalid animation name: \${baseName}\`)
}
}
// Generate animation manifest
const manifest = {
buildTime: new Date().toISOString(),
totalAssets: processedAssets.length,
assets: processedAssets,
schemes: {
artist: processedAssets.map(a => a.artistName),
semantic: processedAssets.map(a => a.semanticName),
legacy: processedAssets.map(a => a.legacyName),
hierarchical: processedAssets.map(a => a.hierarchicalName)
}
}
fs.writeFileSync(
path.join(outputDir, 'animation-manifest.json'),
JSON.stringify(manifest, null, 2)
)
return manifest
}
generateTypescriptConstants(manifest, outputFile) {
const content = \`// Auto-generated animation constants
export const AnimationAssets = {
\${manifest.assets.map(asset => \` '\${asset.semanticName}': {
semantic: '\${asset.semanticName}',
artist: '\${asset.artistName}',
legacy: '\${asset.legacyName}',
hierarchical: '\${asset.hierarchicalName}',
blenderFile: '\${asset.blenderFile}.blend'
}\`).join(',\\n')}
} as const
export type AnimationName = keyof typeof AnimationAssets
\`
fs.writeFileSync(outputFile, content)
console.log(\`Generated TypeScript constants: \${outputFile}\`)
}
}
// Usage in build pipeline
const processor = new AnimationBuildProcessor()
processor.processBlenderAssets('./assets/blender', './dist')
.then(manifest => {
processor.generateTypescriptConstants(manifest, './src/generated/animations.ts')
console.log('Animation build complete!')
})
.catch(console.error)
\`\`\`
## Webpack Plugin Integration
\`\`\`javascript
// webpack-animation-plugin.js
import { AnimationNameMapper } from '@kjanat/owen'
class AnimationValidationPlugin {
constructor(options = {}) {
this.options = {
schemes: ['semantic', 'artist'],
validateOnBuild: true,
generateConstants: true,
...options
}
this.mapper = new AnimationNameMapper()
}
apply(compiler) {
compiler.hooks.afterCompile.tap('AnimationValidationPlugin', (compilation) => {
if (this.options.validateOnBuild) {
this.validateAnimations(compilation)
}
})
if (this.options.generateConstants) {
compiler.hooks.emit.tap('AnimationValidationPlugin', (compilation) => {
this.generateConstants(compilation)
})
}
}
validateAnimations(compilation) {
// Find animation references in source code
const animationReferences = this.findAnimationReferences(compilation)
animationReferences.forEach(ref => {
const validation = this.mapper.validateAnimationName(ref.name)
if (!validation.isValid) {
const error = new Error(
\`Invalid animation name: "\${ref.name}" in \${ref.file}:\${ref.line}\`
)
compilation.errors.push(error)
}
})
}
generateConstants(compilation) {
const constants = this.generateAnimationConstants()
compilation.assets['animations.generated.js'] = {
source: () => constants,
size: () => constants.length
}
}
findAnimationReferences(compilation) {
// Implementation to find animation references in source files
// Returns array of { name, file, line }
return []
}
generateAnimationConstants() {
const schemes = this.options.schemes
let content = '// Auto-generated animation constants\\n\\n'
schemes.forEach(scheme => {
const animations = this.mapper.getAllAnimationsByScheme(scheme)
content += \`export const \${scheme.toUpperCase()}_ANIMATIONS = [\`
content += animations.map(anim => \`'\${anim}'\`).join(', ')
content += \`]\\n\\n\`
})
return content
}
}
// webpack.config.js
import AnimationValidationPlugin from './webpack-animation-plugin.js'
export default {
// ... other config
plugins: [
new AnimationValidationPlugin({
schemes: ['semantic', 'artist'],
validateOnBuild: true,
generateConstants: true
})
]
}
\`\`\`
## Testing Integration
\`\`\`javascript
// animation-test-utils.js
import { AnimationNameMapper, OwenAnimationContext } from '@kjanat/owen'
export class AnimationTestHelper {
constructor(mockGltf) {
this.context = new OwenAnimationContext(mockGltf)
this.mapper = new AnimationNameMapper()
}
expectAnimationExists(animationName) {
expect(() => this.context.getClip(animationName)).not.toThrow()
}
expectAnimationScheme(animationName, expectedScheme) {
const detectedScheme = this.mapper.detectScheme(animationName)
expect(detectedScheme).toBe(expectedScheme)
}
expectConversionWorks(fromName, toScheme, expectedName) {
const converted = this.mapper.convert(fromName, toScheme)
expect(converted).toBe(expectedName)
}
expectRoundTripConversion(animationName) {
const originalScheme = this.mapper.detectScheme(animationName)
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
// Convert through all schemes and back
let current = animationName
schemes.forEach(scheme => {
current = this.mapper.convert(current, scheme)
})
const final = this.mapper.convert(current, originalScheme)
expect(final).toBe(animationName)
}
getAllSchemeVariants(animationName) {
return this.mapper.getAllNames(animationName)
}
}
// Usage in tests
describe('Animation System Integration', () => {
let helper
beforeEach(() => {
helper = new AnimationTestHelper(mockGltf)
})
test('all schemes work', () => {
helper.expectAnimationExists('wait_idle_L')
helper.expectAnimationExists('Owen_WaitIdle')
helper.expectAnimationExists('owen.state.wait.idle.loop')
helper.expectAnimationExists('OwenWaitIdleLoop')
})
test('scheme detection', () => {
helper.expectAnimationScheme('wait_idle_L', 'legacy')
helper.expectAnimationScheme('Owen_WaitIdle', 'artist')
helper.expectAnimationScheme('owen.state.wait.idle.loop', 'hierarchical')
helper.expectAnimationScheme('OwenWaitIdleLoop', 'semantic')
})
test('conversions work correctly', () => {
helper.expectConversionWorks('wait_idle_L', 'semantic', 'OwenWaitIdleLoop')
helper.expectConversionWorks('Owen_ReactAngry', 'legacy', 'react_angry_S')
})
test('round-trip conversions', () => {
helper.expectRoundTripConversion('wait_idle_L')
helper.expectRoundTripConversion('Owen_WaitIdle')
helper.expectRoundTripConversion('OwenSleepPeaceLoop')
})
})
\`\`\`
These integration examples show how to effectively use the multi-scheme animation system in real-world applications and build processes.
`
fs.writeFileSync(path.join(examplesDir, 'integration-examples.md'), content, 'utf8')
console.log('📄 Generated: integration-examples.md')
}
/**
* Get characteristics for a specific scheme
*/
function getSchemeCharacteristics (scheme) {
const characteristics = {
legacy: `
- **Lowercase with underscores**: Easy to type, traditional format
- **Suffix notation**: \`_L\` for Loop, \`_S\` for Short animations
- **Compact names**: Shorter than other schemes
- **Technical focus**: Designed for developers, not artists`,
artist: `
- **Owen prefix**: Consistent branding across all animations
- **PascalCase format**: Easy to read and professional looking
- **Artist-friendly**: No technical jargon or suffixes
- **Blender optimized**: Perfect for animation asset naming`,
hierarchical: `
- **Dot notation**: Clear hierarchical structure
- **Category organization**: Groups related animations logically
- **IDE friendly**: Excellent autocomplete support
- **Extensible**: Easy to add new categories and subcategories`,
semantic: `
- **Self-documenting**: Animation purpose is immediately clear
- **Modern naming**: Follows contemporary naming conventions
- **Descriptive**: Includes context like duration and emotion
- **Code readable**: Perfect for maintainable codebases`
}
return characteristics[scheme] || 'No characteristics defined for this scheme.'
}
/**
* Get best practices for a specific scheme
*/
function getSchemeBestPractices (scheme) {
const practices = {
legacy: `
1. **Maintain suffix consistency**: Always use \`_L\` for loops, \`_S\` for short animations
2. **Use descriptive words**: Choose clear, short words that describe the animation
3. **Follow underscore convention**: Separate words with underscores, keep lowercase
4. **Document duration**: The suffix should accurately reflect animation type`,
artist: `
1. **Always use Owen prefix**: Maintain consistent \`Owen_\` branding
2. **Use PascalCase**: Capitalize each word, no spaces or underscores
3. **Be descriptive**: Choose names that clearly describe the animation's purpose
4. **Keep it simple**: Avoid technical terms, focus on what the animation shows`,
hierarchical: `
1. **Follow the hierarchy**: Use \`owen.category.subcategory.type\` structure
2. **Be consistent**: Use the same categories for similar animations
3. **Plan the structure**: Think about organization before adding new categories
4. **Document categories**: Keep a reference of what each category contains`,
semantic: `
1. **Be descriptive**: Names should clearly indicate the animation's purpose
2. **Include context**: Add emotional state, duration, or other relevant details
3. **Use PascalCase**: Follow modern JavaScript naming conventions
4. **Stay consistent**: Use similar naming patterns for related animations`
}
return practices[scheme] || 'No best practices defined for this scheme.'
}
// Run the script if called directly
if (process.argv[1] === __filename) {
generateSchemeExamples()
.catch(error => {
console.error('💥 Script failed:', error)
process.exit(1)
})
}

View File

@ -0,0 +1,315 @@
#!/usr/bin/env node
/**
* Test Multi-Scheme Functionality Script
* Comprehensive testing of the multi-scheme animation system
*/
import fs from 'fs'
import path from 'path'
import { fileURLToPath, pathToFileURL } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const PROJECT_ROOT = path.resolve(__dirname, '..')
const ANIMATION_MAPPER_PATH = path.join(PROJECT_ROOT, 'src', 'animation', 'AnimationNameMapper.js')
/**
* Run comprehensive multi-scheme tests
*/
async function testMultiSchemes () {
try {
console.log('🧪 Testing Multi-Scheme Animation System...')
// Import the AnimationNameMapper
const animationMapperUrl = pathToFileURL(ANIMATION_MAPPER_PATH)
const { AnimationNameMapper } = await import(animationMapperUrl)
const mapper = new AnimationNameMapper()
const testResults = {
timestamp: new Date().toISOString(),
passed: 0,
failed: 0,
tests: []
}
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
/**
* Add a test result
*/
function addTest (name, passed, details = {}, error = null) {
const test = {
name,
passed,
details,
error: error?.message || error
}
testResults.tests.push(test)
if (passed) {
testResults.passed++
console.log(`${name}`)
} else {
testResults.failed++
console.log(`${name}: ${error}`)
}
return test
}
// Test 1: Basic scheme detection
console.log('\n🔍 Testing scheme detection...')
const testCases = [
{ name: 'wait_idle_L', expectedScheme: 'legacy' },
{ name: 'Owen_ReactAngry', expectedScheme: 'artist' },
{ name: 'owen.state.wait.idle.loop', expectedScheme: 'hierarchical' },
{ name: 'OwenSleepToWaitTransition', expectedScheme: 'semantic' }
]
testCases.forEach(testCase => {
try {
const detectedScheme = mapper.detectScheme(testCase.name)
const passed = detectedScheme === testCase.expectedScheme
addTest(
`Detect scheme for ${testCase.name}`,
passed,
{ detected: detectedScheme, expected: testCase.expectedScheme },
passed ? null : `Expected ${testCase.expectedScheme}, got ${detectedScheme}`
)
} catch (error) {
addTest(`Detect scheme for ${testCase.name}`, false, {}, error)
}
})
// Test 2: Conversion between schemes
console.log('\n🔄 Testing scheme conversions...')
const conversionTests = [
{ from: 'wait_idle_L', to: 'semantic', expected: 'OwenWaitIdleLoop' },
{ from: 'Owen_ReactAngry', to: 'legacy', expected: 'react_angry_L' },
{ from: 'owen.state.sleep.peace.loop', to: 'artist', expected: 'Owen_SleepPeace' },
{ from: 'OwenTypeIdleLoop', to: 'hierarchical', expected: 'owen.state.type.idle.loop' }
]
conversionTests.forEach(test => {
try {
const result = mapper.convert(test.from, test.to)
const passed = result === test.expected
addTest(
`Convert ${test.from} to ${test.to}`,
passed,
{ result, expected: test.expected },
passed ? null : `Expected ${test.expected}, got ${result}`
)
} catch (error) {
addTest(`Convert ${test.from} to ${test.to}`, false, {}, error)
}
})
// Test 3: Round-trip conversions (should return to original)
console.log('\n🔁 Testing round-trip conversions...')
const roundTripTests = ['wait_idle_L', 'Owen_ReactAngry', 'owen.state.sleep.peace.loop', 'OwenTypeIdleLoop']
roundTripTests.forEach(originalName => {
try {
const originalScheme = mapper.detectScheme(originalName)
let currentName = originalName
// Convert through all other schemes and back
const otherSchemes = schemes.filter(s => s !== originalScheme)
for (const scheme of otherSchemes) {
currentName = mapper.convert(currentName, scheme)
}
// Convert back to original scheme
const finalName = mapper.convert(currentName, originalScheme)
const passed = finalName === originalName
addTest(
`Round-trip conversion for ${originalName}`,
passed,
{ original: originalName, final: finalName, path: `${originalName} -> ${otherSchemes.join(' -> ')} -> ${originalName}` },
passed ? null : `Round-trip failed: ${originalName} -> ${finalName}`
)
} catch (error) {
addTest(`Round-trip conversion for ${originalName}`, false, {}, error)
}
})
// Test 4: Validation functionality
console.log('\n✅ Testing validation...')
const validationTests = [
{ name: 'wait_idle_L', shouldBeValid: true },
{ name: 'Owen_ValidAnimation', shouldBeValid: true },
{ name: 'invalid_animation_name', shouldBeValid: false },
{ name: 'NotAnAnimation', shouldBeValid: false }
]
validationTests.forEach(test => {
try {
const validation = mapper.validateAnimationName(test.name)
const passed = validation.isValid === test.shouldBeValid
addTest(
`Validate ${test.name}`,
passed,
{ validation, expected: test.shouldBeValid },
passed ? null : `Expected valid=${test.shouldBeValid}, got valid=${validation.isValid}`
)
} catch (error) {
addTest(`Validate ${test.name}`, false, {}, error)
}
})
// Test 5: Get all animations by scheme
console.log('\n📋 Testing animation retrieval...')
schemes.forEach(scheme => {
try {
const animations = mapper.getAllAnimationsByScheme(scheme)
const passed = Array.isArray(animations) && animations.length > 0
addTest(
`Get all ${scheme} animations`,
passed,
{ count: animations.length, sample: animations.slice(0, 3) },
passed ? null : 'No animations returned or invalid format'
)
} catch (error) {
addTest(`Get all ${scheme} animations`, false, {}, error)
}
})
// Test 6: Performance test
console.log('\n⚡ Testing performance...')
const performanceAnimations = ['wait_idle_L', 'Owen_ReactAngry', 'owen.state.sleep.peace.loop']
try {
const iterations = 1000
const startTime = Date.now()
for (let i = 0; i < iterations; i++) {
performanceAnimations.forEach(anim => {
mapper.convert(anim, 'semantic')
mapper.validateAnimationName(anim)
})
}
const endTime = Date.now()
const totalTime = endTime - startTime
const operationsPerSecond = Math.round((iterations * performanceAnimations.length * 2) / (totalTime / 1000))
const passed = totalTime < 5000 // Should complete in under 5 seconds
addTest(
'Performance test',
passed,
{
iterations,
totalTimeMs: totalTime,
operationsPerSecond,
averageTimePerOperation: totalTime / (iterations * performanceAnimations.length * 2)
},
passed ? null : `Performance too slow: ${totalTime}ms for ${iterations} iterations`
)
} catch (error) {
addTest('Performance test', false, {}, error)
}
// Test 7: Edge cases
console.log('\n🎯 Testing edge cases...')
const edgeCases = [
{ name: '', description: 'empty string' },
{ name: 'single', description: 'single word' },
{ name: 'UPPERCASE_NAME_L', description: 'uppercase legacy' },
{ name: 'owen_special_case_S', description: 'legacy with Owen prefix' }
]
edgeCases.forEach(testCase => {
try {
const validation = mapper.validateAnimationName(testCase.name)
// These should generally be invalid or have suggestions
const passed = true // Just test that it doesn't crash
addTest(
`Edge case: ${testCase.description}`,
passed,
{ input: testCase.name, validation },
null
)
} catch (error) {
// It's okay if edge cases throw errors, as long as they're handled gracefully
const passed = error.message && error.message.length > 0
addTest(
`Edge case: ${testCase.description}`,
passed,
{ input: testCase.name },
passed ? null : 'Unhandled error or empty error message'
)
}
})
// Generate final report
const report = {
...testResults,
summary: {
total: testResults.passed + testResults.failed,
passed: testResults.passed,
failed: testResults.failed,
passRate: Math.round((testResults.passed / (testResults.passed + testResults.failed)) * 100),
status: testResults.failed === 0 ? 'PASS' : 'FAIL'
},
environment: {
nodeVersion: process.version,
platform: process.platform,
timestamp: new Date().toISOString()
}
}
// Save report
const reportsDir = path.join(PROJECT_ROOT, 'reports')
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true })
}
fs.writeFileSync(
path.join(reportsDir, 'multi-scheme-test-results.json'),
JSON.stringify(report, null, 2),
'utf8'
)
// Print summary
console.log('\n📊 MULTI-SCHEME TEST SUMMARY')
console.log('='.repeat(50))
console.log(`Status: ${report.summary.status}`)
console.log(`Tests: ${report.summary.passed}/${report.summary.total} passed (${report.summary.passRate}%)`)
console.log(`Failed: ${report.summary.failed}`)
if (testResults.failed > 0) {
console.log('\n❌ FAILED TESTS:')
testResults.tests
.filter(test => !test.passed)
.forEach(test => {
console.log(`${test.name}: ${test.error}`)
})
}
console.log(`\n📁 Full report saved to: ${path.join(reportsDir, 'multi-scheme-test-results.json')}`)
// Exit with error if tests failed
if (testResults.failed > 0) {
process.exit(1)
}
return report
} catch (error) {
console.error('❌ Error running multi-scheme tests:', error.message)
process.exit(1)
}
}
// Run the script if called directly
if (process.argv[1] === __filename) {
testMultiSchemes()
.then(report => {
console.log('✅ Multi-scheme testing complete!')
})
.catch(error => {
console.error('💥 Script failed:', error)
process.exit(1)
})
}

View File

@ -0,0 +1,168 @@
#!/usr/bin/env node
/**
* Animation Name Validation Script
* Validates all animation names across different schemes
*/
import { AnimationNameMapper } from '../src/animation/AnimationNameMapper.js'
import { writeFile, mkdir } from 'fs/promises'
import { existsSync } from 'fs'
const PRIMARY_SCHEME = process.env.PRIMARY_SCHEME || 'semantic'
async function validateAnimations () {
console.log('🔍 Validating animation naming schemes...')
console.log(`Primary scheme: ${PRIMARY_SCHEME}`)
const mapper = new AnimationNameMapper()
const results = {
timestamp: new Date().toISOString(),
primaryScheme: PRIMARY_SCHEME,
validations: [],
errors: [],
warnings: [],
summary: {}
}
try {
// Get all animations from the mapper
const allAnimations = mapper.getAllAnimations()
console.log(`Found ${allAnimations.length} animations to validate`)
// Validate each animation
for (const animation of allAnimations) {
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
for (const scheme of schemes) {
const animName = animation[scheme]
if (!animName) continue
try {
const validation = mapper.validateAnimationName(animName)
results.validations.push({
name: animName,
scheme,
isValid: validation.isValid,
detectedScheme: validation.scheme,
error: validation.error
})
if (!validation.isValid) {
results.errors.push({
name: animName,
scheme,
error: validation.error,
suggestions: validation.suggestions
})
}
// Check for scheme mismatches
if (validation.isValid && validation.scheme !== scheme) {
results.warnings.push({
name: animName,
expectedScheme: scheme,
detectedScheme: validation.scheme,
message: `Animation name "${animName}" expected to be in ${scheme} scheme but detected as ${validation.scheme}`
})
}
} catch (error) {
results.errors.push({
name: animName,
scheme,
error: error.message
})
}
}
}
// Test conversions between schemes
console.log('🔄 Testing scheme conversions...')
for (const animation of allAnimations) {
const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
for (const fromScheme of schemes) {
for (const toScheme of schemes) {
if (fromScheme === toScheme) continue
const sourceName = animation[fromScheme]
if (!sourceName) continue
try {
const converted = mapper.convert(sourceName, toScheme)
const expected = animation[toScheme]
if (converted !== expected) {
results.errors.push({
name: sourceName,
scheme: `${fromScheme}->${toScheme}`,
error: `Conversion mismatch: expected "${expected}", got "${converted}"`
})
}
} catch (error) {
results.errors.push({
name: sourceName,
scheme: `${fromScheme}->${toScheme}`,
error: `Conversion failed: ${error.message}`
})
}
}
}
}
// Generate summary
results.summary = {
totalAnimations: allAnimations.length,
totalValidations: results.validations.length,
validAnimations: results.validations.filter(v => v.isValid).length,
invalidAnimations: results.errors.length,
warnings: results.warnings.length,
successRate: ((results.validations.filter(v => v.isValid).length / results.validations.length) * 100).toFixed(2)
}
// Create reports directory if it doesn't exist
if (!existsSync('reports')) {
await mkdir('reports', { recursive: true })
}
// Write results to file
await writeFile('reports/animation-validation.json', JSON.stringify(results, null, 2))
// Print summary
console.log('\n📊 Validation Summary:')
console.log(`✅ Valid animations: ${results.summary.validAnimations}/${results.summary.totalValidations}`)
console.log(`❌ Invalid animations: ${results.summary.invalidAnimations}`)
console.log(`⚠️ Warnings: ${results.summary.warnings}`)
console.log(`📈 Success rate: ${results.summary.successRate}%`)
if (results.errors.length > 0) {
console.log('\n❌ Errors found:')
results.errors.forEach(error => {
console.log(` - ${error.name} (${error.scheme}): ${error.error}`)
})
}
if (results.warnings.length > 0) {
console.log('\n⚠ Warnings:')
results.warnings.forEach(warning => {
console.log(` - ${warning.message}`)
})
}
// Exit with error if validation failed
if (results.errors.length > 0) {
console.log('\n💥 Validation failed with errors')
process.exit(1)
} else {
console.log('\n✅ All animations validated successfully')
process.exit(0)
}
} catch (error) {
console.error('💥 Validation script failed:', error)
process.exit(1)
}
}
if (import.meta.url === `file://${process.argv[1]}`) {
validateAnimations()
}

View File

@ -0,0 +1,441 @@
#!/usr/bin/env node
/**
* Animation Processing Validation Script
*
* Validates animations processed from Blender or other sources to ensure:
* - Proper naming scheme compliance
* - File integrity and format validation
* - Asset optimization and size requirements
* - Integration with existing animation system
*
* Used by GitHub Actions animation-processing workflow
*/
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Configuration
const CONFIG = {
animationsDir: path.join(__dirname, '..', 'assets', 'animations'),
reportsDir: path.join(__dirname, '..', 'reports'),
maxFileSize: 5 * 1024 * 1024, // 5MB max per animation file
supportedFormats: ['.gltf', '.glb', '.fbx', '.json'],
requiredMetadata: ['name', 'duration', 'frameRate'],
namingSchemes: ['legacy', 'artist', 'hierarchical', 'semantic']
}
/**
* Validation result structure
*/
class ValidationResult {
constructor () {
this.passed = []
this.failed = []
this.warnings = []
this.stats = {
totalFiles: 0,
validFiles: 0,
invalidFiles: 0,
totalSize: 0,
averageSize: 0
}
}
addPass (file, message) {
this.passed.push({ file, message, timestamp: new Date().toISOString() })
this.stats.validFiles++
}
addFail (file, message, error = null) {
this.failed.push({
file,
message,
error: error?.message || error,
timestamp: new Date().toISOString()
})
this.stats.invalidFiles++
}
addWarning (file, message) {
this.warnings.push({ file, message, timestamp: new Date().toISOString() })
}
updateStats (size) {
this.stats.totalFiles++
this.stats.totalSize += size
this.stats.averageSize = this.stats.totalSize / this.stats.totalFiles
}
isValid () {
return this.failed.length === 0
}
getSummary () {
return {
success: this.isValid(),
summary: {
passed: this.passed.length,
failed: this.failed.length,
warnings: this.warnings.length,
...this.stats
},
details: {
passed: this.passed,
failed: this.failed,
warnings: this.warnings
}
}
}
}
/**
* File format validators
*/
const Validators = {
async validateGLTF (filePath) {
try {
const content = await fs.readFile(filePath, 'utf8')
const gltf = JSON.parse(content)
// Basic GLTF structure validation
if (!gltf.asset || !gltf.asset.version) {
throw new Error('Invalid GLTF: Missing asset version')
}
if (!gltf.animations || !Array.isArray(gltf.animations)) {
throw new Error('Invalid GLTF: Missing or invalid animations array')
}
return {
valid: true,
animations: gltf.animations.length,
version: gltf.asset.version
}
} catch (error) {
return { valid: false, error: error.message }
}
},
async validateGLB (filePath) {
try {
const stats = await fs.stat(filePath)
const buffer = await fs.readFile(filePath)
// Basic GLB header validation (magic number: 0x46546C67 = "glTF")
if (buffer.length < 12) {
throw new Error('Invalid GLB: File too small')
}
const magic = buffer.readUInt32LE(0)
if (magic !== 0x46546C67) {
throw new Error('Invalid GLB: Invalid magic number')
}
const version = buffer.readUInt32LE(4)
if (version !== 2) {
throw new Error(`Invalid GLB: Unsupported version ${version}`)
}
return {
valid: true,
size: stats.size,
version
}
} catch (error) {
return { valid: false, error: error.message }
}
},
async validateJSON (filePath) {
try {
const content = await fs.readFile(filePath, 'utf8')
const data = JSON.parse(content)
// Check for required animation metadata
if (!data.name || !data.duration) {
throw new Error('Invalid animation JSON: Missing required metadata')
}
return {
valid: true,
metadata: data
}
} catch (error) {
return { valid: false, error: error.message }
}
}
}
/**
* Validates naming scheme compliance
*/
function validateNamingScheme (filename) {
const baseName = path.parse(filename).name
const schemes = {
legacy: /^[a-z][a-z0-9_]*$/,
artist: /^[A-Z][a-zA-Z0-9_]*$/,
hierarchical: /^[a-z]+(\.[a-z]+)*$/,
semantic: /^[a-z]+(_[a-z]+)*$/
}
const matchedSchemes = []
for (const [scheme, pattern] of Object.entries(schemes)) {
if (pattern.test(baseName)) {
matchedSchemes.push(scheme)
}
}
return {
valid: matchedSchemes.length > 0,
schemes: matchedSchemes,
name: baseName
}
}
/**
* Validates individual animation file
*/
async function validateAnimationFile (filePath) {
const result = new ValidationResult()
const filename = path.basename(filePath)
const ext = path.extname(filePath).toLowerCase()
try {
// Check file existence
const stats = await fs.stat(filePath)
result.updateStats(stats.size)
// Check file size
if (stats.size > CONFIG.maxFileSize) {
result.addWarning(filename,
`File size ${(stats.size / 1024 / 1024).toFixed(2)}MB exceeds recommended maximum ${CONFIG.maxFileSize / 1024 / 1024}MB`
)
}
// Check supported format
if (!CONFIG.supportedFormats.includes(ext)) {
result.addFail(filename, `Unsupported file format: ${ext}`)
return result
}
// Validate naming scheme
const namingValidation = validateNamingScheme(filename)
if (!namingValidation.valid) {
result.addFail(filename, 'Filename does not match any supported naming scheme')
} else {
result.addPass(filename, `Naming scheme compliance: ${namingValidation.schemes.join(', ')}`)
}
// Format-specific validation
let formatValidation = { valid: true }
switch (ext) {
case '.gltf':
formatValidation = await Validators.validateGLTF(filePath)
break
case '.glb':
formatValidation = await Validators.validateGLB(filePath)
break
case '.json':
formatValidation = await Validators.validateJSON(filePath)
break
}
if (formatValidation.valid) {
result.addPass(filename, `Valid ${ext.toUpperCase()} format`)
} else {
result.addFail(filename, `Invalid ${ext.toUpperCase()} format: ${formatValidation.error}`)
}
} catch (error) {
result.addFail(filename, `Validation error: ${error.message}`, error)
}
return result
}
/**
* Scans directory for animation files
*/
async function scanAnimationFiles (directory) {
const files = []
try {
const entries = await fs.readdir(directory, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(directory, entry.name)
if (entry.isDirectory()) {
// Recursively scan subdirectories
const subFiles = await scanAnimationFiles(fullPath)
files.push(...subFiles)
} else if (entry.isFile()) {
const ext = path.extname(entry.name).toLowerCase()
if (CONFIG.supportedFormats.includes(ext)) {
files.push(fullPath)
}
}
}
} catch (error) {
console.warn(`Warning: Could not scan directory ${directory}: ${error.message}`)
}
return files
}
/**
* Generates validation report
*/
async function generateReport (result, outputPath) {
const report = {
timestamp: new Date().toISOString(),
validation: result.getSummary(),
recommendations: []
}
// Add recommendations based on results
if (result.failed.length > 0) {
report.recommendations.push('Fix failed validations before proceeding with deployment')
}
if (result.warnings.length > 0) {
report.recommendations.push('Review warnings to optimize animation assets')
}
if (result.stats.averageSize > 1024 * 1024) {
report.recommendations.push('Consider optimizing large animation files for better performance')
}
// Ensure reports directory exists
await fs.mkdir(path.dirname(outputPath), { recursive: true })
// Write report
await fs.writeFile(outputPath, JSON.stringify(report, null, 2))
return report
}
/**
* Main validation function
*/
async function validateProcessedAnimations () {
console.log('🔍 Validating processed animation assets...')
const overallResult = new ValidationResult()
try {
// Check if animations directory exists
try {
await fs.access(CONFIG.animationsDir)
} catch {
console.warn(`⚠️ Animations directory not found: ${CONFIG.animationsDir}`)
console.log('✅ No processed animations to validate')
return true
}
// Scan for animation files
console.log(`📁 Scanning ${CONFIG.animationsDir}...`)
const animationFiles = await scanAnimationFiles(CONFIG.animationsDir)
if (animationFiles.length === 0) {
console.log('✅ No animation files found to validate')
return true
}
console.log(`📄 Found ${animationFiles.length} animation files`)
// Validate each file
for (let i = 0; i < animationFiles.length; i++) {
const file = animationFiles[i]
const relativePath = path.relative(CONFIG.animationsDir, file)
console.log(`📝 Validating ${i + 1}/${animationFiles.length}: ${relativePath}`)
const fileResult = await validateAnimationFile(file)
// Merge results
overallResult.passed.push(...fileResult.passed)
overallResult.failed.push(...fileResult.failed)
overallResult.warnings.push(...fileResult.warnings)
overallResult.stats.totalFiles += fileResult.stats.totalFiles
overallResult.stats.validFiles += fileResult.stats.validFiles
overallResult.stats.invalidFiles += fileResult.stats.invalidFiles
overallResult.stats.totalSize += fileResult.stats.totalSize
}
// Calculate average size
if (overallResult.stats.totalFiles > 0) {
overallResult.stats.averageSize = overallResult.stats.totalSize / overallResult.stats.totalFiles
}
// Generate report
const reportPath = path.join(CONFIG.reportsDir, 'processed-animations-validation.json')
await generateReport(overallResult, reportPath)
// Print summary
console.log('\n📊 Validation Summary:')
console.log(`✅ Passed: ${overallResult.passed.length}`)
console.log(`❌ Failed: ${overallResult.failed.length}`)
console.log(`⚠️ Warnings: ${overallResult.warnings.length}`)
console.log(`📁 Total files: ${overallResult.stats.totalFiles}`)
console.log(`📦 Total size: ${(overallResult.stats.totalSize / 1024 / 1024).toFixed(2)}MB`)
console.log(`📄 Average size: ${(overallResult.stats.averageSize / 1024).toFixed(2)}KB`)
// Print failures in detail
if (overallResult.failed.length > 0) {
console.log('\n❌ Validation Failures:')
overallResult.failed.forEach(failure => {
console.log(`${failure.file}: ${failure.message}`)
})
}
// Print warnings
if (overallResult.warnings.length > 0) {
console.log('\n⚠ Validation Warnings:')
overallResult.warnings.forEach(warning => {
console.log(`${warning.file}: ${warning.message}`)
})
}
console.log(`\n📋 Full report saved to: ${reportPath}`)
const isValid = overallResult.isValid()
console.log(isValid ? '\n✅ All validations passed!' : '\n❌ Validation failed!')
return isValid
} catch (error) {
console.error('💥 Validation process failed:', error.message)
// Generate error report
const errorReport = {
timestamp: new Date().toISOString(),
success: false,
error: error.message,
stack: error.stack
}
const reportPath = path.join(CONFIG.reportsDir, 'processed-animations-validation.json')
try {
await fs.mkdir(path.dirname(reportPath), { recursive: true })
await fs.writeFile(reportPath, JSON.stringify(errorReport, null, 2))
} catch (reportError) {
console.error('Failed to write error report:', reportError.message)
}
return false
}
}
/**
* CLI execution
*/
if (import.meta.url === `file://${process.argv[1]}`) {
const success = await validateProcessedAnimations()
process.exit(success ? 0 : 1)
}
export { validateProcessedAnimations, validateAnimationFile, ValidationResult }