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

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

View File

@ -0,0 +1,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 }