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
Release / Validate Version (push) Has been cancelled
Release / Build and Test (push) Has been cancelled
Release / Create Release (push) Has been cancelled
Release / Publish to NPM (push) Has been cancelled
Release / Deploy Demo (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
Animation Processing Pipeline / Update Animation Documentation (push) Has been cancelled
Animation Processing Pipeline / Deploy Animation Demo (push) Has been cancelled
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
Release / Validate Version (push) Has been cancelled
Release / Build and Test (push) Has been cancelled
Release / Create Release (push) Has been cancelled
Release / Publish to NPM (push) Has been cancelled
Release / Deploy Demo (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
Animation Processing Pipeline / Update Animation Documentation (push) Has been cancelled
Animation Processing Pipeline / Deploy Animation Demo (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:
268
scripts/blender-animation-processor.py
Normal file
268
scripts/blender-animation-processor.py
Normal 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()
|
||||
361
scripts/check-naming-conflicts.js
Normal file
361
scripts/check-naming-conflicts.js
Normal 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)
|
||||
})
|
||||
}
|
||||
433
scripts/convert-animation-names.js
Normal file
433
scripts/convert-animation-names.js
Normal 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)
|
||||
})
|
||||
}
|
||||
252
scripts/generate-animation-constants.js
Normal file
252
scripts/generate-animation-constants.js
Normal 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)
|
||||
})
|
||||
}
|
||||
1556
scripts/generate-animation-docs.js
Normal file
1556
scripts/generate-animation-docs.js
Normal file
File diff suppressed because it is too large
Load Diff
802
scripts/generate-scheme-examples.js
Normal file
802
scripts/generate-scheme-examples.js
Normal 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)
|
||||
})
|
||||
}
|
||||
315
scripts/test-multi-schemes.js
Normal file
315
scripts/test-multi-schemes.js
Normal 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)
|
||||
})
|
||||
}
|
||||
168
scripts/validate-animations.js
Normal file
168
scripts/validate-animations.js
Normal 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()
|
||||
}
|
||||
441
scripts/validate-processed-animations.js
Normal file
441
scripts/validate-processed-animations.js
Normal 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 }
|
||||
Reference in New Issue
Block a user