+
š¤ Owen Animation System Demo
-
-
-
-
-
Owen is typing...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
System initializing...
-
-
+
+
+
+
+
+ Owen is typing...
+
-
-
Owen System Status
-
State: Initializing
-
Emotion: Neutral
-
Last Activity: Now
-
Active Clips: 0
+
+
+
+
+
+
-
-
-
Initializing...
-
Neutral
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Enter an animation name above to see conversions...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
System initializing...
+
+
+
+
+
+
Owen System Status
+
+ State: Initializing
+
+
+ Emotion: Neutral
+
+
+ Last Activity: Now
+
+
+ Active Clips: 0
+
+
+
+
+
+
Initializing...
+
Neutral
+
-
+
diff --git a/package-lock.json b/package-lock.json
index b1c346f..42e356b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,17 +1,18 @@
{
- "name": "owen-animation-system",
- "version": "1.0.0",
+ "name": "@kjanat/owen",
+ "version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "owen-animation-system",
- "version": "1.0.0",
+ "name": "@kjanat/owen",
+ "version": "1.0.1",
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
"dependencies": {
"three": "^0.176.0"
},
"devDependencies": {
+ "@playwright/test": "^1.52.0",
"jsdoc": "^4.0.2",
"pre-commit": "^1.2.2",
"standard": "*",
@@ -790,6 +791,22 @@
"node": ">= 8"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz",
+ "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.52.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz",
@@ -4126,6 +4143,53 @@
"node": ">=4"
}
},
+ "node_modules/playwright": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz",
+ "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.52.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz",
+ "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
diff --git a/package.json b/package.json
index e7d05bd..3cc0e94 100644
--- a/package.json
+++ b/package.json
@@ -1,51 +1,65 @@
{
- "name": "@kjanat/owen",
- "version": "1.0.1",
- "description": "A comprehensive Three.js animation system for character state management with clean architecture principles",
- "main": "src/index.js",
- "types": "src/index.d.ts",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "dev:host": "vite --host",
- "build": "vite build",
- "preview": "vite preview",
- "lint": "standard",
- "lint:fix": "standard --fix",
- "docs": "jsdoc -c jsdoc.config.json",
- "format": "npx prettier --ignore-path --write '**/*.{html,css}' 'docs/**/*.{html,css}'"
- },
- "keywords": [
- "three.js",
- "animation",
- "state-machine",
- "character",
- "gltf",
- "3d"
- ],
- "author": "Kaj \"@kjanat\" Kowalski",
- "license": "AGPL-3.0-only OR LicenseRef-Commercial",
- "dependencies": {
- "three": "^0.176.0"
- },
- "devDependencies": {
- "jsdoc": "^4.0.2",
- "pre-commit": "^1.2.2",
- "standard": "*",
- "vite": "^6.3.5"
- },
- "engines": {
- "node": ">=16.0.0"
- },
- "standard": {
- "globals": [
- "requestAnimationFrame"
+ "name": "@kjanat/owen",
+ "version": "1.0.1",
+ "description": "A comprehensive Three.js animation system for character state management with clean architecture principles",
+ "main": "src/index.js",
+ "types": "src/index.d.ts",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "dev:host": "vite --host",
+ "build": "vite build",
+ "build:demo": "vite build --config vite.demo.config.js",
+ "preview": "vite preview",
+ "lint": "standard",
+ "lint:fix": "standard --fix",
+ "docs": "jsdoc -c jsdoc.config.json",
+ "format": "npx prettier --ignore-path --write '**/*.{html,css}' 'docs/**/*.{html,css}'",
+ "validate:animations": "node scripts/validate-animations.js",
+ "generate:constants": "node scripts/generate-animation-constants.js",
+ "check:conflicts": "node scripts/check-naming-conflicts.js",
+ "test:schemes": "node scripts/test-multi-schemes.js",
+ "animation:validate": "npm run validate:animations && npm run check:conflicts",
+ "animation:generate": "npm run generate:constants && npm run validate:animations",
+ "preview:demo": "vite preview --config vite.demo.config.js --port 3000",
+ "test": "npx playwright test",
+ "test:demo": "npx playwright test tests/demo.spec.js",
+ "test:pages": "npx playwright test tests/pages.spec.js",
+ "test:ui": "npx playwright test --ui",
+ "test:headed": "npx playwright test --headed"
+ },
+ "keywords": [
+ "three.js",
+ "animation",
+ "state-machine",
+ "character",
+ "gltf",
+ "3d"
+ ],
+ "author": "Kaj \"@kjanat\" Kowalski",
+ "license": "AGPL-3.0-only OR LicenseRef-Commercial",
+ "dependencies": {
+ "three": "^0.176.0"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.52.0",
+ "jsdoc": "^4.0.2",
+ "pre-commit": "^1.2.2",
+ "standard": "*",
+ "vite": "^6.3.5"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "standard": {
+ "globals": [
+ "requestAnimationFrame"
+ ]
+ },
+ "pre-commit": [
+ "lint:fix",
+ "lint",
+ "docs",
+ "format"
]
- },
- "pre-commit": [
- "lint:fix",
- "lint",
- "docs",
- "format"
- ]
}
diff --git a/playwright.config.js b/playwright.config.js
new file mode 100644
index 0000000..e239507
--- /dev/null
+++ b/playwright.config.js
@@ -0,0 +1,92 @@
+import { defineConfig, devices } from '@playwright/test'
+
+/**
+ * @see https://playwright.dev/docs/test-configuration
+ */
+export default defineConfig({
+ testDir: './tests',
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: process.env.CI ? 'github' : 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: 'http://localhost:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+
+ /* Take screenshot on failure */
+ screenshot: 'only-on-failure',
+
+ /* Record video on failure */
+ video: 'retain-on-failure'
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] }
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] }
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] }
+ },
+
+ /* Test against mobile viewports. */
+ {
+ name: 'Mobile Chrome',
+ use: { ...devices['Pixel 5'] }
+ },
+ {
+ name: 'Mobile Safari',
+ use: { ...devices['iPhone 12'] }
+ },
+
+ /* Test against branded browsers. */
+ {
+ name: 'Microsoft Edge',
+ use: { ...devices['Desktop Edge'], channel: 'msedge' }
+ },
+ {
+ name: 'Google Chrome',
+ use: { ...devices['Desktop Chrome'], channel: 'chrome' }
+ }
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'npm run preview:demo',
+ port: 3000,
+ reuseExistingServer: !process.env.CI,
+ timeout: 120000
+ },
+
+ /* Global test timeout */
+ timeout: 30000,
+
+ /* Global test expect timeout */
+ expect: {
+ timeout: 5000
+ },
+
+ /* Output directory for test results */
+ outputDir: 'test-results/',
+
+ /* Test timeout per test */
+ globalTimeout: 600000
+})
diff --git a/scripts/blender-animation-processor.py b/scripts/blender-animation-processor.py
new file mode 100644
index 0000000..e6e6f7e
--- /dev/null
+++ b/scripts/blender-animation-processor.py
@@ -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()
diff --git a/scripts/check-naming-conflicts.js b/scripts/check-naming-conflicts.js
new file mode 100644
index 0000000..ed01cff
--- /dev/null
+++ b/scripts/check-naming-conflicts.js
@@ -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)
+ })
+}
diff --git a/scripts/convert-animation-names.js b/scripts/convert-animation-names.js
new file mode 100644
index 0000000..637d82e
--- /dev/null
+++ b/scripts/convert-animation-names.js
@@ -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
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 Animation name to convert
+ --to, -t Target scheme (legacy|artist|hierarchical|semantic)
+ --all-schemes, -a Convert to all schemes
+ --validate, -v Validate the animation name
+
+FILE CONVERSION:
+ --input, -i Input file with animation names (JSON or line-separated)
+ --output, -o Output file for results (optional)
+ --to, -t Target scheme for conversion
+
+BATCH OPERATIONS:
+ --batch, -b Convert all animations in the system
+ --output, -o 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)
+ })
+}
diff --git a/scripts/generate-animation-constants.js b/scripts/generate-animation-constants.js
new file mode 100644
index 0000000..7019fc9
--- /dev/null
+++ b/scripts/generate-animation-constants.js
@@ -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)
+ })
+}
diff --git a/scripts/generate-animation-docs.js b/scripts/generate-animation-docs.js
new file mode 100644
index 0000000..4850605
--- /dev/null
+++ b/scripts/generate-animation-docs.js
@@ -0,0 +1,1556 @@
+#!/usr/bin/env node
+
+/**
+ * Generate Animation Documentation Script
+ * Creates comprehensive documentation for the 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')
+
+/**
+ * Generate comprehensive animation documentation
+ */
+async function generateAnimationDocs () {
+ try {
+ console.log('š Generating Animation Documentation...')
+
+ // Import the AnimationNameMapper
+ const animationMapperUrl = pathToFileURL(ANIMATION_MAPPER_PATH)
+ const { AnimationNameMapper } = await import(animationMapperUrl)
+ const mapper = new AnimationNameMapper()
+
+ const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
+ const timestamp = new Date().toISOString()
+
+ // Gather animation data
+ const animationData = {}
+ schemes.forEach(scheme => {
+ animationData[scheme] = mapper.getAllAnimationsByScheme(scheme)
+ })
+
+ // Generate main documentation
+ await generateMainDocumentation(animationData, timestamp)
+
+ // Generate API reference
+ await generateAPIReference(mapper, animationData, timestamp)
+
+ // Generate scheme comparison
+ await generateSchemeComparison(mapper, animationData, timestamp)
+
+ // Generate migration guide
+ await generateMigrationGuide(mapper, animationData, timestamp)
+
+ // Generate examples
+ await generateExamples(mapper, animationData, timestamp)
+
+ console.log('ā
Animation documentation generated successfully!')
+ } catch (error) {
+ console.error('ā Error generating animation documentation:', error.message)
+ process.exit(1)
+ }
+}
+
+/**
+ * Generate the main animation documentation
+ */
+async function generateMainDocumentation (animationData, timestamp) {
+ const totalAnimations = Object.values(animationData).reduce((sum, arr) => sum + arr.length, 0)
+
+ const content = `# Owen Animation System Documentation
+
+*Generated: ${timestamp}*
+
+The Owen Animation System provides a comprehensive multi-scheme approach to animation naming that supports backward compatibility while offering modern, developer-friendly alternatives.
+
+## Overview
+
+- **Total Animations**: ${totalAnimations}
+- **Naming Schemes**: 4 (Legacy, Artist, Hierarchical, Semantic)
+- **Bidirectional Conversion**: ā
+- **Auto-Detection**: ā
+- **Validation**: ā
+
+## Naming Schemes
+
+### 1. Legacy Scheme
+*Format: \`word_word_L/S\`*
+
+Traditional naming used in earlier versions. Includes suffix indicating Loop (L) or Short (S) animations.
+
+**Examples:**
+${animationData.legacy.slice(0, 5).map(name => `- \`${name}\``).join('\n')}
+
+### 2. Artist Scheme
+*Format: \`Owen_PascalCase\`*
+
+Artist-friendly naming that's easy to read and use in Blender or other animation tools.
+
+**Examples:**
+${animationData.artist.slice(0, 5).map(name => `- \`${name}\``).join('\n')}
+
+### 3. Hierarchical Scheme
+*Format: \`owen.category.subcategory\`*
+
+Organized, hierarchical naming that groups related animations logically.
+
+**Examples:**
+${animationData.hierarchical.slice(0, 5).map(name => `- \`${name}\``).join('\n')}
+
+### 4. Semantic Scheme
+*Format: \`OwenDescriptiveName\`*
+
+Modern, semantic naming that clearly describes the animation's purpose.
+
+**Examples:**
+${animationData.semantic.slice(0, 5).map(name => `- \`${name}\``).join('\n')}
+
+## Quick Start
+
+\`\`\`javascript
+import { OwenAnimationContext, AnimationNameMapper } from '@kjanat/owen'
+
+// Using the animation context with multi-scheme support
+const context = new OwenAnimationContext(gltf)
+
+// Load animation using any scheme
+context.getClip('wait_idle_L') // Legacy
+context.getClip('Owen_WaitIdle') // Artist
+context.getClip('owen.state.wait.idle.loop') // Hierarchical
+context.getClip('OwenWaitIdleLoop') // Semantic
+
+// Convert between schemes
+const mapper = new AnimationNameMapper()
+const semantic = mapper.convert('wait_idle_L', 'semantic')
+console.log(semantic) // 'OwenWaitIdleLoop'
+\`\`\`
+
+## Validation and Error Handling
+
+The system provides comprehensive validation:
+
+\`\`\`javascript
+const validation = mapper.validateAnimationName('unknown_animation')
+console.log(validation.isValid) // false
+console.log(validation.suggestions) // ['wait_idle_L', 'react_angry_L', ...]
+console.log(validation.errors) // ['Animation not found in any scheme']
+\`\`\`
+
+## Animation Categories
+
+### State Animations
+- **Wait**: Idle states and waiting animations
+- **React**: Reaction animations to stimuli
+- **Sleep**: Sleep and rest state animations
+- **Type**: Typing and work-related animations
+
+### Emotion Variants
+- **Neutral**: Default emotional state
+- **Happy**: Positive emotional expressions
+- **Angry**: Negative emotional expressions
+- **Peace**: Calm and peaceful states
+
+### Duration Types
+- **Loop (L)**: Continuous looping animations
+- **Short (S)**: Brief, one-time animations
+- **Transition**: Animations between states
+
+## Best Practices
+
+1. **Choose the Right Scheme**: Use semantic for new code, artist for Blender workflow
+2. **Validate Names**: Always validate animation names in production
+3. **Handle Errors**: Provide fallbacks for missing animations
+4. **Use Constants**: Import animation constants for type safety
+
+\`\`\`javascript
+import { SemanticAnimations } from '@kjanat/owen'
+
+// Type-safe animation names
+context.getClip(SemanticAnimations.WAIT_IDLE_LOOP)
+\`\`\`
+
+## Integration with Workflows
+
+### Blender Integration
+Use the artist scheme (\`Owen_AnimationName\`) in Blender for the best artist experience.
+
+### Code Integration
+Use the semantic scheme (\`OwenAnimationName\`) in code for clarity and maintainability.
+
+### Legacy Support
+The system automatically detects and converts legacy names for backward compatibility.
+
+## Performance
+
+- **Conversion Speed**: >10,000 operations/second
+- **Memory Usage**: <50MB for full animation set
+- **Auto-Detection**: <1ms per animation name
+
+## See Also
+
+- [API Reference](./API_REFERENCE.md)
+- [Scheme Comparison](./SCHEME_COMPARISON.md)
+- [Migration Guide](./MIGRATION_GUIDE.md)
+- [Examples](./EXAMPLES.md)
+`
+
+ const docsDir = path.join(PROJECT_ROOT, 'docs')
+ if (!fs.existsSync(docsDir)) {
+ fs.mkdirSync(docsDir, { recursive: true })
+ }
+
+ fs.writeFileSync(path.join(docsDir, 'ANIMATION_SYSTEM.md'), content, 'utf8')
+ console.log('š Generated: ANIMATION_SYSTEM.md')
+}
+
+/**
+ * Generate API reference documentation
+ */
+async function generateAPIReference (mapper, animationData, timestamp) {
+ const content = `# Animation System API Reference
+
+*Generated: ${timestamp}*
+
+## AnimationNameMapper
+
+Core class for converting and validating animation names across different schemes.
+
+### Constructor
+
+\`\`\`javascript
+const mapper = new AnimationNameMapper()
+\`\`\`
+
+### Methods
+
+#### convert(animationName, targetScheme)
+
+Converts an animation name to the target scheme.
+
+**Parameters:**
+- \`animationName\` (string): The animation name to convert
+- \`targetScheme\` (string): Target scheme ('legacy', 'artist', 'hierarchical', 'semantic')
+
+**Returns:** \`string\` - The converted animation name
+
+**Throws:** \`Error\` - If animation not found or conversion fails
+
+**Example:**
+\`\`\`javascript
+const semantic = mapper.convert('wait_idle_L', 'semantic')
+// Returns: 'OwenWaitIdleLoop'
+\`\`\`
+
+#### detectScheme(animationName)
+
+Automatically detects the naming scheme of an animation.
+
+**Parameters:**
+- \`animationName\` (string): The animation name to analyze
+
+**Returns:** \`string\` - Detected scheme name
+
+**Example:**
+\`\`\`javascript
+const scheme = mapper.detectScheme('Owen_ReactAngry')
+// Returns: 'artist'
+\`\`\`
+
+#### validateAnimationName(animationName)
+
+Validates an animation name and provides suggestions.
+
+**Parameters:**
+- \`animationName\` (string): The animation name to validate
+
+**Returns:** \`Object\` - Validation result
+- \`isValid\` (boolean): Whether the name is valid
+- \`detectedScheme\` (string|null): Detected scheme if valid
+- \`suggestions\` (string[]): Similar valid animation names
+- \`errors\` (string[]): Error messages
+
+**Example:**
+\`\`\`javascript
+const validation = mapper.validateAnimationName('unknown_anim')
+console.log(validation.isValid) // false
+console.log(validation.suggestions) // ['wait_idle_L', 'react_angry_L']
+\`\`\`
+
+#### getAllAnimationsByScheme(scheme)
+
+Gets all animation names for a specific scheme.
+
+**Parameters:**
+- \`scheme\` (string): The scheme name
+
+**Returns:** \`string[]\` - Array of animation names
+
+**Example:**
+\`\`\`javascript
+const semanticAnims = mapper.getAllAnimationsByScheme('semantic')
+// Returns: ['OwenWaitIdleLoop', 'OwenReactAngryShort', ...]
+\`\`\`
+
+#### getAllNames(animationName)
+
+Gets all scheme variants of an animation name.
+
+**Parameters:**
+- \`animationName\` (string): Any valid animation name
+
+**Returns:** \`Object\` - Names in all schemes
+- \`legacy\` (string): Legacy scheme name
+- \`artist\` (string): Artist scheme name
+- \`hierarchical\` (string): Hierarchical scheme name
+- \`semantic\` (string): Semantic scheme name
+
+**Example:**
+\`\`\`javascript
+const allNames = mapper.getAllNames('wait_idle_L')
+console.log(allNames.semantic) // 'OwenWaitIdleLoop'
+console.log(allNames.artist) // 'Owen_WaitIdle'
+\`\`\`
+
+## OwenAnimationContext (Enhanced)
+
+Enhanced animation context with multi-scheme support.
+
+### New Methods
+
+#### getClipByScheme(animationName, scheme)
+
+Gets animation clip using a specific scheme.
+
+**Parameters:**
+- \`animationName\` (string): Animation name in the specified scheme
+- \`scheme\` (string): The naming scheme to use
+
+**Returns:** \`AnimationClip\` - The animation clip
+
+#### getAnimationNames(scheme)
+
+Gets all available animation names in a specific scheme.
+
+**Parameters:**
+- \`scheme\` (string): The naming scheme
+
+**Returns:** \`string[]\` - Available animation names
+
+#### validateAnimationName(animationName)
+
+Validates an animation name and returns suggestions.
+
+**Parameters:**
+- \`animationName\` (string): Name to validate
+
+**Returns:** \`Object\` - Validation result
+
+#### getAnimationsByStateAndEmotion(state, emotion, scheme)
+
+Filters animations by state and emotion.
+
+**Parameters:**
+- \`state\` (string): Animation state ('wait', 'react', 'sleep', 'type')
+- \`emotion\` (string|null): Emotion filter ('happy', 'angry', 'peace')
+- \`scheme\` (string): Naming scheme to use (default: 'semantic')
+
+**Returns:** \`string[]\` - Matching animation names
+
+## Animation Constants
+
+Type-safe constants for all naming schemes.
+
+### Enums
+
+#### NamingSchemes
+\`\`\`javascript
+export const NamingSchemes = {
+ LEGACY: 'legacy',
+ ARTIST: 'artist',
+ HIERARCHICAL: 'hierarchical',
+ SEMANTIC: 'semantic'
+}
+\`\`\`
+
+### Animation Constants
+
+#### LegacyAnimations
+Constants for legacy scheme animations.
+
+\`\`\`javascript
+import { LegacyAnimations } from '@kjanat/owen'
+
+context.getClip(LegacyAnimations.WAIT_IDLE_L)
+\`\`\`
+
+#### ArtistAnimations
+Constants for artist scheme animations.
+
+\`\`\`javascript
+import { ArtistAnimations } from '@kjanat/owen'
+
+context.getClip(ArtistAnimations.WAIT_IDLE)
+\`\`\`
+
+#### HierarchicalAnimations
+Constants for hierarchical scheme animations.
+
+\`\`\`javascript
+import { HierarchicalAnimations } from '@kjanat/owen'
+
+context.getClip(HierarchicalAnimations.STATE_WAIT_IDLE_LOOP)
+\`\`\`
+
+#### SemanticAnimations
+Constants for semantic scheme animations.
+
+\`\`\`javascript
+import { SemanticAnimations } from '@kjanat/owen'
+
+context.getClip(SemanticAnimations.WAIT_IDLE_LOOP)
+\`\`\`
+
+### Utility Functions
+
+#### convertAnimationName(animationName, targetScheme)
+Convenience function for animation conversion.
+
+#### getAllAnimationNames(scheme)
+Get all animations for a scheme.
+
+#### validateAnimationName(animationName)
+Validate an animation name.
+
+#### getAnimationsByStateAndEmotion(state, emotion, scheme)
+Filter animations by criteria.
+
+## Error Handling
+
+All methods throw descriptive errors for invalid inputs:
+
+\`\`\`javascript
+try {
+ const converted = mapper.convert('invalid_name', 'semantic')
+} catch (error) {
+ console.error('Conversion failed:', error.message)
+ // Handle the error appropriately
+}
+\`\`\`
+
+## Performance Notes
+
+- Animation lookups are O(1) for direct scheme access
+- Conversions are O(1) using pre-built mapping tables
+- Validation includes fuzzy matching for suggestions
+- Memory usage scales linearly with animation count
+`
+
+ const docsDir = path.join(PROJECT_ROOT, 'docs')
+ fs.writeFileSync(path.join(docsDir, 'API_REFERENCE.md'), content, 'utf8')
+ console.log('š Generated: API_REFERENCE.md')
+}
+
+/**
+ * Generate scheme comparison documentation
+ */
+async function generateSchemeComparison (mapper, animationData, timestamp) {
+ const content = `# Animation Naming Schemes Comparison
+
+*Generated: ${timestamp}*
+
+This document compares the four naming schemes supported by the Owen Animation System.
+
+## Scheme Overview
+
+| Scheme | Format | Use Case | Example |
+|--------|--------|----------|---------|
+| **Legacy** | \`word_word_L/S\` | Backward compatibility | \`wait_idle_L\` |
+| **Artist** | \`Owen_PascalCase\` | Blender workflow | \`Owen_WaitIdle\` |
+| **Hierarchical** | \`owen.category.subcategory\` | Organized structure | \`owen.state.wait.idle.loop\` |
+| **Semantic** | \`OwenDescriptiveName\` | Modern development | \`OwenWaitIdleLoop\` |
+
+## Detailed Comparison
+
+### Legacy Scheme
+**Format:** \`lowercase_words_L/S\`
+
+**Pros:**
+- Maintains compatibility with existing code
+- Clear loop/short distinction with suffix
+- Compact and simple
+
+**Cons:**
+- Not immediately readable
+- Limited expressiveness
+- Technical suffix may confuse artists
+
+**Best for:** Maintaining existing codebases, migration scenarios
+
+**Examples:**
+${animationData.legacy.slice(0, 8).map(name => `- \`${name}\``).join('\n')}
+
+### Artist Scheme
+**Format:** \`Owen_PascalCase\`
+
+**Pros:**
+- Easy to read and understand
+- Perfect for Blender asset naming
+- Artist-friendly without technical jargon
+- Consistent Owen branding
+
+**Cons:**
+- Longer names than legacy
+- Less structural organization
+- Requires prefix for all animations
+
+**Best for:** Blender workflows, artist collaboration, asset management
+
+**Examples:**
+${animationData.artist.slice(0, 8).map(name => `- \`${name}\``).join('\n')}
+
+### Hierarchical Scheme
+**Format:** \`owen.category.subcategory.type\`
+
+**Pros:**
+- Excellent organization and grouping
+- IDE autocomplete friendly
+- Clear categorization
+- Extensible structure
+
+**Cons:**
+- Longer names
+- May be verbose for simple animations
+- Requires understanding of hierarchy
+
+**Best for:** Large animation sets, organized codebases, tooling integration
+
+**Examples:**
+${animationData.hierarchical.slice(0, 8).map(name => `- \`${name}\``).join('\n')}
+
+### Semantic Scheme
+**Format:** \`OwenDescriptiveName\`
+
+**Pros:**
+- Highly readable and self-documenting
+- Modern naming convention
+- Clear intent and purpose
+- Easy to understand without documentation
+
+**Cons:**
+- Can become quite long
+- No enforced structure
+- Potential naming inconsistencies
+
+**Best for:** New development, API design, maintainable codebases
+
+**Examples:**
+${animationData.semantic.slice(0, 8).map(name => `- \`${name}\``).join('\n')}
+
+## Conversion Examples
+
+The following table shows how the same animation appears in each scheme:
+
+| Legacy | Artist | Hierarchical | Semantic |
+|--------|--------|--------------|----------|
+${animationData.legacy.slice(0, 5).map(legacyName => {
+ try {
+ const artist = mapper.convert(legacyName, 'artist')
+ const hierarchical = mapper.convert(legacyName, 'hierarchical')
+ const semantic = mapper.convert(legacyName, 'semantic')
+ return `| \`${legacyName}\` | \`${artist}\` | \`${hierarchical}\` | \`${semantic}\` |`
+ } catch {
+ return `| \`${legacyName}\` | - | - | - |`
+ }
+}).join('\n')}
+
+## Usage Recommendations
+
+### For New Projects
+**Recommended:** Semantic scheme for code, Artist scheme for assets
+
+\`\`\`javascript
+// In code - use semantic for clarity
+import { SemanticAnimations } from '@kjanat/owen'
+context.getClip(SemanticAnimations.WAIT_IDLE_LOOP)
+
+// In Blender - use artist scheme
+// Asset name: Owen_WaitIdle.blend
+\`\`\`
+
+### For Existing Projects
+**Recommended:** Keep legacy scheme, add semantic for new animations
+
+\`\`\`javascript
+// Existing code continues to work
+context.getClip('wait_idle_L')
+
+// New code can use semantic
+context.getClip('OwenNewAnimationLoop')
+\`\`\`
+
+### For Animation Teams
+**Recommended:** Artist scheme for all Blender work
+
+- Consistent \`Owen_\` prefix
+- Easy to read animation names
+- No technical suffixes to confuse artists
+- Direct mapping to code equivalents
+
+### For Large Codebases
+**Recommended:** Hierarchical scheme for organization
+
+\`\`\`javascript
+// Clear organization
+context.getClip('owen.state.wait.idle.loop')
+context.getClip('owen.state.wait.active.loop')
+context.getClip('owen.reaction.happy.short')
+context.getClip('owen.reaction.angry.short')
+\`\`\`
+
+## Migration Strategies
+
+### Gradual Migration
+1. Start using new scheme for new animations
+2. Convert high-traffic animations first
+3. Use the mapper to support both old and new names
+4. Update documentation and examples
+
+### Asset Pipeline Integration
+1. Use artist scheme in Blender
+2. Automatically convert to semantic in build pipeline
+3. Generate constants file for type safety
+4. Update references in code
+
+### Team Adoption
+1. Train artists on artist scheme conventions
+2. Train developers on semantic scheme benefits
+3. Use validation tools to ensure consistency
+4. Establish naming guidelines and review processes
+
+## Performance Comparison
+
+| Operation | Legacy | Artist | Hierarchical | Semantic |
+|-----------|---------|---------|--------------|----------|
+| **Lookup Speed** | Fast | Fast | Fast | Fast |
+| **Memory Usage** | Low | Medium | High | Medium |
+| **Readability** | Low | High | Medium | High |
+| **Maintainability** | Low | Medium | High | High |
+
+## Conclusion
+
+Each naming scheme serves different needs:
+
+- **Legacy**: Essential for backward compatibility
+- **Artist**: Perfect for Blender and asset workflows
+- **Hierarchical**: Best for large, organized codebases
+- **Semantic**: Ideal for modern, readable code
+
+The multi-scheme system allows teams to use the most appropriate scheme for each context while maintaining full interoperability.
+`
+
+ const docsDir = path.join(PROJECT_ROOT, 'docs')
+ fs.writeFileSync(path.join(docsDir, 'SCHEME_COMPARISON.md'), content, 'utf8')
+ console.log('š Generated: SCHEME_COMPARISON.md')
+}
+
+/**
+ * Generate migration guide
+ */
+async function generateMigrationGuide (mapper, animationData, timestamp) {
+ const content = `# Migration Guide
+
+*Generated: ${timestamp}*
+
+This guide helps you migrate existing Owen Animation System code to use the new multi-scheme naming system.
+
+## Overview
+
+The multi-scheme system is **100% backward compatible**. Your existing code will continue to work without changes while giving you access to modern naming schemes.
+
+## Migration Scenarios
+
+### Scenario 1: Existing Project (No Changes Needed)
+
+If you have existing code using legacy names, **no changes are required**:
+
+\`\`\`javascript
+// This continues to work exactly as before
+const context = new OwenAnimationContext(gltf)
+context.getClip('wait_idle_L') // ā
Works
+context.getClip('react_angry_S') // ā
Works
+context.getClip('sleep_peace_L') // ā
Works
+\`\`\`
+
+### Scenario 2: Gradual Modernization
+
+Start using semantic names for new animations while keeping legacy names:
+
+\`\`\`javascript
+// Existing animations - keep legacy names
+context.getClip('wait_idle_L')
+
+// New animations - use semantic names
+context.getClip('OwenNewFeatureIdleLoop')
+context.getClip('OwenSpecialReactionShort')
+\`\`\`
+
+### Scenario 3: Full Migration to Semantic
+
+Replace legacy names with semantic equivalents:
+
+**Before:**
+\`\`\`javascript
+context.getClip('wait_idle_L')
+context.getClip('react_angry_S')
+context.getClip('sleep_peace_L')
+\`\`\`
+
+**After:**
+\`\`\`javascript
+context.getClip('OwenWaitIdleLoop')
+context.getClip('OwenReactAngryShort')
+context.getClip('OwenSleepPeaceLoop')
+\`\`\`
+
+### Scenario 4: Artist Workflow Integration
+
+Update your Blender to code pipeline:
+
+**Blender Assets (Artist Scheme):**
+- \`Owen_WaitIdle.blend\`
+- \`Owen_ReactAngry.blend\`
+- \`Owen_SleepPeace.blend\`
+
+**Code (Semantic Scheme):**
+\`\`\`javascript
+// Automatic conversion happens behind the scenes
+context.getClip('OwenWaitIdleLoop') // Finds Owen_WaitIdle asset
+context.getClip('OwenReactAngryShort') // Finds Owen_ReactAngry asset
+context.getClip('OwenSleepPeaceLoop') // Finds Owen_SleepPeace asset
+\`\`\`
+
+## Step-by-Step Migration
+
+### Step 1: Update Owen Package
+
+Ensure you have the latest version with multi-scheme support:
+
+\`\`\`bash
+npm update @kjanat/owen
+\`\`\`
+
+### Step 2: Optional - Add Type Safety
+
+Import animation constants for type safety:
+
+\`\`\`javascript
+import { SemanticAnimations, LegacyAnimations } from '@kjanat/owen'
+
+// Type-safe animation names
+context.getClip(SemanticAnimations.WAIT_IDLE_LOOP)
+context.getClip(LegacyAnimations.WAIT_IDLE_L)
+\`\`\`
+
+### Step 3: Optional - Use Animation Mapper
+
+For advanced use cases, use the mapper directly:
+
+\`\`\`javascript
+import { AnimationNameMapper } from '@kjanat/owen'
+
+const mapper = new AnimationNameMapper()
+
+// Convert legacy to semantic
+const semantic = mapper.convert('wait_idle_L', 'semantic')
+console.log(semantic) // 'OwenWaitIdleLoop'
+
+// Validate animation names
+const validation = mapper.validateAnimationName('my_animation')
+if (!validation.isValid) {
+ console.log('Suggestions:', validation.suggestions)
+}
+\`\`\`
+
+### Step 4: Optional - Update Asset Pipeline
+
+If you use build tools, integrate automatic conversion:
+
+\`\`\`javascript
+// Build script example
+import { AnimationNameMapper } from '@kjanat/owen'
+
+const mapper = new AnimationNameMapper()
+
+// Convert Blender asset names to code-friendly names
+const blenderName = 'Owen_WaitIdle'
+const codeName = mapper.convert(blenderName, 'semantic')
+// Use codeName in your generated code/configs
+\`\`\`
+
+## Common Migration Patterns
+
+### Pattern 1: Animation Loading with Fallbacks
+
+\`\`\`javascript
+function loadAnimation(context, animationName) {
+ try {
+ return context.getClip(animationName)
+ } catch (error) {
+ // Try converting to different schemes as fallback
+ const mapper = new AnimationNameMapper()
+
+ try {
+ const semantic = mapper.convert(animationName, 'semantic')
+ return context.getClip(semantic)
+ } catch {
+ console.warn(\`Animation not found: \${animationName}\`)
+ return context.getClip('OwenWaitIdleLoop') // Default fallback
+ }
+ }
+}
+\`\`\`
+
+### Pattern 2: Dynamic Animation Selection
+
+\`\`\`javascript
+import { AnimationNameMapper } from '@kjanat/owen'
+
+const mapper = new AnimationNameMapper()
+
+function getAnimationsByEmotion(emotion, scheme = 'semantic') {
+ return mapper.getAllAnimationsByScheme(scheme)
+ .filter(anim => anim.toLowerCase().includes(emotion.toLowerCase()))
+}
+
+// Usage
+const angryAnimations = getAnimationsByEmotion('angry')
+console.log(angryAnimations) // ['OwenReactAngryShort', 'OwenWaitAngryLoop', ...]
+\`\`\`
+
+### Pattern 3: Validation in Development
+
+\`\`\`javascript
+import { AnimationNameMapper } from '@kjanat/owen'
+
+const mapper = new AnimationNameMapper()
+
+function validateAndLoadAnimation(context, animationName) {
+ const validation = mapper.validateAnimationName(animationName)
+
+ if (!validation.isValid) {
+ console.warn(\`Invalid animation: \${animationName}\`)
+ console.log('Did you mean:', validation.suggestions.slice(0, 3))
+ return null
+ }
+
+ return context.getClip(animationName)
+}
+\`\`\`
+
+## Updating Existing Code
+
+### Search and Replace Patterns
+
+For bulk updates, you can use these search patterns:
+
+**Legacy to Semantic:**
+- \`wait_idle_L\` ā \`OwenWaitIdleLoop\`
+- \`react_angry_S\` ā \`OwenReactAngryShort\`
+- \`sleep_peace_L\` ā \`OwenSleepPeaceLoop\`
+- \`type_idle_L\` ā \`OwenTypeIdleLoop\`
+
+**Script for Automatic Conversion:**
+\`\`\`bash
+# Use the conversion script
+node scripts/convert-animation-names.js --input my-animations.json --to semantic --output converted.json
+\`\`\`
+
+### Code Analysis
+
+Use the validation script to analyze your codebase:
+
+\`\`\`bash
+# Check for naming conflicts
+node scripts/check-naming-conflicts.js
+
+# Test multi-scheme functionality
+node scripts/test-multi-schemes.js
+\`\`\`
+
+## Testing Your Migration
+
+### Unit Tests
+
+\`\`\`javascript
+import { OwenAnimationContext, AnimationNameMapper } from '@kjanat/owen'
+
+describe('Animation Migration', () => {
+ let context, mapper
+
+ beforeEach(() => {
+ context = new OwenAnimationContext(mockGltf)
+ mapper = new AnimationNameMapper()
+ })
+
+ test('legacy animations still work', () => {
+ expect(() => context.getClip('wait_idle_L')).not.toThrow()
+ })
+
+ test('semantic animations work', () => {
+ expect(() => context.getClip('OwenWaitIdleLoop')).not.toThrow()
+ })
+
+ test('conversion consistency', () => {
+ const legacy = 'wait_idle_L'
+ const semantic = mapper.convert(legacy, 'semantic')
+ const backToLegacy = mapper.convert(semantic, 'legacy')
+
+ expect(backToLegacy).toBe(legacy)
+ })
+})
+\`\`\`
+
+### Integration Tests
+
+\`\`\`javascript
+describe('Multi-scheme Integration', () => {
+ test('same animation different schemes', () => {
+ const animations = [
+ 'wait_idle_L',
+ 'Owen_WaitIdle',
+ 'owen.state.wait.idle.loop',
+ 'OwenWaitIdleLoop'
+ ]
+
+ // All should resolve to the same clip
+ const clips = animations.map(anim => context.getClip(anim))
+ expect(clips.every(clip => clip === clips[0])).toBe(true)
+ })
+})
+\`\`\`
+
+## Rollback Strategy
+
+If you need to rollback changes:
+
+1. **No Code Changes**: If you only added new schemes, remove them from constants
+2. **Code Changes**: Revert to using only legacy names - the system will still work
+3. **Asset Changes**: Rename assets back to legacy format
+
+The system is designed to be safe for gradual adoption and easy rollback.
+
+## Team Migration Guide
+
+### For Developers
+1. Learn the semantic scheme for new code
+2. Use validation tools during development
+3. Add type-safe constants to catch errors early
+4. Review naming consistency in code reviews
+
+### For Artists
+1. Adopt the artist scheme (\`Owen_AnimationName\`) in Blender
+2. Use descriptive, readable names
+3. Follow the Owen prefix convention
+4. Test animations with the validation tools
+
+### For Technical Artists
+1. Set up build pipeline integration
+2. Configure automatic name conversion
+3. Establish validation workflows
+4. Create documentation for team processes
+
+## Troubleshooting
+
+### Common Issues
+
+**Issue**: Animation not found after migration
+**Solution**: Check if the name was converted correctly using the mapper
+
+**Issue**: Build pipeline broken
+**Solution**: Ensure asset names follow the chosen scheme consistently
+
+**Issue**: Team confusion about which scheme to use
+**Solution**: Establish clear guidelines - semantic for code, artist for assets
+
+### Getting Help
+
+- Check animation name with: \`mapper.validateAnimationName(name)\`
+- View all available names: \`mapper.getAllAnimationsByScheme(scheme)\`
+- Convert between schemes: \`mapper.convert(name, targetScheme)\`
+
+## Next Steps
+
+After migration:
+1. Update team documentation
+2. Establish naming guidelines
+3. Set up automated validation
+4. Train team members on new workflows
+5. Monitor for consistency in code reviews
+
+The multi-scheme system grows with your project and team needs!
+`
+
+ const docsDir = path.join(PROJECT_ROOT, 'docs')
+ fs.writeFileSync(path.join(docsDir, 'MIGRATION_GUIDE.md'), content, 'utf8')
+ console.log('š Generated: MIGRATION_GUIDE.md')
+}
+
+/**
+ * Generate comprehensive examples
+ */
+async function generateExamples (mapper, animationData, timestamp) {
+ const content = `# Animation System Examples
+
+*Generated: ${timestamp}*
+
+This document provides comprehensive examples of using the Owen Animation System with multiple naming schemes.
+
+## Basic Usage Examples
+
+### Loading Animations
+
+\`\`\`javascript
+import { OwenAnimationContext } from '@kjanat/owen'
+
+const context = new OwenAnimationContext(gltf)
+
+// Legacy scheme
+const idleClip = context.getClip('wait_idle_L')
+
+// Artist scheme
+const reactClip = context.getClip('Owen_ReactAngry')
+
+// Hierarchical scheme
+const sleepClip = context.getClip('owen.state.sleep.peace.loop')
+
+// Semantic scheme
+const typeClip = context.getClip('OwenTypeIdleLoop')
+\`\`\`
+
+### Using Animation Constants
+
+\`\`\`javascript
+import {
+ LegacyAnimations,
+ ArtistAnimations,
+ SemanticAnimations,
+ HierarchicalAnimations
+} from '@kjanat/owen'
+
+// Type-safe animation loading
+context.getClip(LegacyAnimations.WAIT_IDLE_L)
+context.getClip(ArtistAnimations.REACT_ANGRY)
+context.getClip(SemanticAnimations.WAIT_IDLE_LOOP)
+context.getClip(HierarchicalAnimations.STATE_SLEEP_PEACE_LOOP)
+\`\`\`
+
+## Animation Name Conversion
+
+### Basic Conversion
+
+\`\`\`javascript
+import { AnimationNameMapper } from '@kjanat/owen'
+
+const mapper = new AnimationNameMapper()
+
+// Convert legacy to semantic
+const semantic = mapper.convert('wait_idle_L', 'semantic')
+console.log(semantic) // 'OwenWaitIdleLoop'
+
+// Convert artist to hierarchical
+const hierarchical = mapper.convert('Owen_ReactAngry', 'hierarchical')
+console.log(hierarchical) // 'owen.state.react.angry.short'
+
+// Convert semantic to legacy
+const legacy = mapper.convert('OwenSleepPeaceLoop', 'legacy')
+console.log(legacy) // 'sleep_peace_L'
+\`\`\`
+
+### Batch Conversion
+
+\`\`\`javascript
+const legacyNames = ['wait_idle_L', 'react_angry_S', 'sleep_peace_L']
+
+const semanticNames = legacyNames.map(name =>
+ mapper.convert(name, 'semantic')
+)
+
+console.log(semanticNames)
+// ['OwenWaitIdleLoop', 'OwenReactAngryShort', 'OwenSleepPeaceLoop']
+\`\`\`
+
+### Get All Scheme Variants
+
+\`\`\`javascript
+const allVariants = mapper.getAllNames('wait_idle_L')
+
+console.log(allVariants)
+// {
+// legacy: 'wait_idle_L',
+// artist: 'Owen_WaitIdle',
+// hierarchical: 'owen.state.wait.idle.loop',
+// semantic: 'OwenWaitIdleLoop'
+// }
+\`\`\`
+
+## Validation Examples
+
+### Basic Validation
+
+\`\`\`javascript
+const validation = mapper.validateAnimationName('unknown_animation')
+
+console.log(validation.isValid) // false
+console.log(validation.detectedScheme) // null
+console.log(validation.suggestions) // ['wait_idle_L', 'react_angry_L', ...]
+console.log(validation.errors) // ['Animation not found in any scheme']
+\`\`\`
+
+### Validation with Error Handling
+
+\`\`\`javascript
+function safeLoadAnimation(context, animationName) {
+ const validation = mapper.validateAnimationName(animationName)
+
+ if (validation.isValid) {
+ return context.getClip(animationName)
+ } else {
+ console.warn(\`Invalid animation: \${animationName}\`)
+
+ if (validation.suggestions.length > 0) {
+ console.log('Did you mean:', validation.suggestions[0])
+ return context.getClip(validation.suggestions[0])
+ }
+
+ // Fallback to default animation
+ return context.getClip('OwenWaitIdleLoop')
+ }
+}
+\`\`\`
+
+## Scheme Detection
+
+### Automatic Detection
+
+\`\`\`javascript
+const animations = [
+ 'wait_idle_L', // legacy
+ 'Owen_ReactAngry', // artist
+ 'owen.state.sleep.peace.loop', // hierarchical
+ 'OwenTypeIdleLoop' // semantic
+]
+
+animations.forEach(anim => {
+ const scheme = mapper.detectScheme(anim)
+ console.log(\`\${anim} -> \${scheme}\`)
+})
+\`\`\`
+
+### Scheme-Specific Processing
+
+\`\`\`javascript
+function processAnimationByScheme(animationName) {
+ const scheme = mapper.detectScheme(animationName)
+
+ switch (scheme) {
+ case 'legacy':
+ console.log('Processing legacy animation:', animationName)
+ break
+ case 'artist':
+ console.log('Processing artist animation:', animationName)
+ break
+ case 'hierarchical':
+ console.log('Processing hierarchical animation:', animationName)
+ break
+ case 'semantic':
+ console.log('Processing semantic animation:', animationName)
+ break
+ default:
+ console.log('Unknown scheme for:', animationName)
+ }
+}
+\`\`\`
+
+## Filtering and Searching
+
+### Get Animations by Scheme
+
+\`\`\`javascript
+// Get all semantic animations
+const semanticAnimations = mapper.getAllAnimationsByScheme('semantic')
+console.log('Semantic animations:', semanticAnimations.length)
+
+// Get all artist animations
+const artistAnimations = mapper.getAllAnimationsByScheme('artist')
+console.log('Artist animations:', artistAnimations.length)
+\`\`\`
+
+### Filter by State and Emotion
+
+\`\`\`javascript
+import { getAnimationsByStateAndEmotion } from '@kjanat/owen'
+
+// Get all wait animations
+const waitAnimations = getAnimationsByStateAndEmotion('wait')
+console.log('Wait animations:', waitAnimations)
+
+// Get angry react animations
+const angryReactions = getAnimationsByStateAndEmotion('react', 'angry')
+console.log('Angry reactions:', angryReactions)
+
+// Get peaceful sleep animations in hierarchical scheme
+const peacefulSleep = getAnimationsByStateAndEmotion('sleep', 'peace', 'hierarchical')
+console.log('Peaceful sleep:', peacefulSleep)
+\`\`\`
+
+### Custom Filtering
+
+\`\`\`javascript
+function findAnimationsByKeyword(keyword, scheme = 'semantic') {
+ const allAnimations = mapper.getAllAnimationsByScheme(scheme)
+ return allAnimations.filter(anim =>
+ anim.toLowerCase().includes(keyword.toLowerCase())
+ )
+}
+
+// Find all idle animations
+const idleAnimations = findAnimationsByKeyword('idle')
+
+// Find all loop animations
+const loopAnimations = findAnimationsByKeyword('loop')
+\`\`\`
+
+## Advanced Integration Patterns
+
+### Animation State Machine
+
+\`\`\`javascript
+class CharacterAnimationState {
+ constructor(context) {
+ this.context = context
+ this.mapper = new AnimationNameMapper()
+ this.currentState = 'idle'
+ this.currentEmotion = 'neutral'
+ }
+
+ setState(state, emotion = this.currentEmotion) {
+ this.currentState = state
+ this.currentEmotion = emotion
+
+ // Find appropriate animation
+ const animations = getAnimationsByStateAndEmotion(state, emotion, 'semantic')
+
+ if (animations.length > 0) {
+ const animationName = animations[0] // Pick first match
+ const clip = this.context.getClip(animationName)
+
+ console.log(\`Transitioning to: \${animationName}\`)
+ return clip
+ } else {
+ console.warn(\`No animation found for state: \${state}, emotion: \${emotion}\`)
+ return this.context.getClip('OwenWaitIdleLoop') // Default fallback
+ }
+ }
+}
+
+// Usage
+const character = new CharacterAnimationState(context)
+character.setState('react', 'angry') // Plays OwenReactAngryShort
+character.setState('sleep', 'peace') // Plays OwenSleepPeaceLoop
+\`\`\`
+
+### Animation Preloader
+
+\`\`\`javascript
+class AnimationPreloader {
+ constructor(context) {
+ this.context = context
+ this.mapper = new AnimationNameMapper()
+ this.preloadedClips = new Map()
+ }
+
+ preloadScheme(scheme) {
+ const animations = this.mapper.getAllAnimationsByScheme(scheme)
+
+ animations.forEach(animName => {
+ try {
+ const clip = this.context.getClip(animName)
+ this.preloadedClips.set(animName, clip)
+ console.log(\`Preloaded: \${animName}\`)
+ } catch (error) {
+ console.warn(\`Failed to preload: \${animName}\`)
+ }
+ })
+
+ return this.preloadedClips.size
+ }
+
+ getClip(animationName) {
+ // Try preloaded first
+ if (this.preloadedClips.has(animationName)) {
+ return this.preloadedClips.get(animationName)
+ }
+
+ // Fall back to context
+ return this.context.getClip(animationName)
+ }
+}
+
+// Usage
+const preloader = new AnimationPreloader(context)
+preloader.preloadScheme('semantic') // Preload all semantic animations
+\`\`\`
+
+### Cross-Scheme Animation Manager
+
+\`\`\`javascript
+class CrossSchemeAnimationManager {
+ constructor(context) {
+ this.context = context
+ this.mapper = new AnimationNameMapper()
+ }
+
+ playAnimation(animationName, preferredScheme = 'semantic') {
+ try {
+ // Try the animation as-is first
+ return this.context.getClip(animationName)
+ } catch (error) {
+ console.log(\`Direct lookup failed for: \${animationName}\`)
+
+ // Try converting to preferred scheme
+ try {
+ const converted = this.mapper.convert(animationName, preferredScheme)
+ console.log(\`Converted \${animationName} to \${converted}\`)
+ return this.context.getClip(converted)
+ } catch (conversionError) {
+ // Try all schemes
+ const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
+
+ for (const scheme of schemes) {
+ try {
+ const converted = this.mapper.convert(animationName, scheme)
+ console.log(\`Found in \${scheme} scheme: \${converted}\`)
+ return this.context.getClip(converted)
+ } catch {
+ continue
+ }
+ }
+
+ throw new Error(\`Animation not found in any scheme: \${animationName}\`)
+ }
+ }
+ }
+}
+\`\`\`
+
+## React/Vue Integration Examples
+
+### React Hook
+
+\`\`\`jsx
+import { useState, useEffect } from 'react'
+import { AnimationNameMapper } from '@kjanat/owen'
+
+function useAnimationValidator(initialAnimation = '') {
+ const [animationName, setAnimationName] = useState(initialAnimation)
+ const [validation, setValidation] = useState(null)
+ const [mapper] = useState(() => new AnimationNameMapper())
+
+ useEffect(() => {
+ if (animationName) {
+ const result = mapper.validateAnimationName(animationName)
+ setValidation(result)
+ }
+ }, [animationName, mapper])
+
+ return {
+ animationName,
+ setAnimationName,
+ validation,
+ isValid: validation?.isValid ?? false,
+ suggestions: validation?.suggestions ?? []
+ }
+}
+
+// Component usage
+function AnimationSelector() {
+ const { animationName, setAnimationName, isValid, suggestions } = useAnimationValidator()
+
+ return (
+
+
setAnimationName(e.target.value)}
+ placeholder="Enter animation name"
+ />
+ {!isValid && animationName && (
+
+
Invalid animation name
+ {suggestions.length > 0 && (
+
+
Suggestions:
+ {suggestions.map(suggestion => (
+
+ ))}
+
+ )}
+
+ )}
+
+ )
+}
+\`\`\`
+
+### Vue Composable
+
+\`\`\`javascript
+import { ref, computed, watch } from 'vue'
+import { AnimationNameMapper } from '@kjanat/owen'
+
+export function useAnimationSchemes() {
+ const mapper = new AnimationNameMapper()
+ const currentAnimation = ref('')
+ const currentScheme = ref('semantic')
+
+ const validation = computed(() => {
+ if (!currentAnimation.value) return null
+ return mapper.validateAnimationName(currentAnimation.value)
+ })
+
+ const convertedAnimations = computed(() => {
+ if (!currentAnimation.value || !validation.value?.isValid) return {}
+
+ try {
+ return mapper.getAllNames(currentAnimation.value)
+ } catch {
+ return {}
+ }
+ })
+
+ function convertToScheme(targetScheme) {
+ if (!currentAnimation.value) return ''
+
+ try {
+ return mapper.convert(currentAnimation.value, targetScheme)
+ } catch {
+ return ''
+ }
+ }
+
+ return {
+ currentAnimation,
+ currentScheme,
+ validation,
+ convertedAnimations,
+ convertToScheme,
+ isValid: computed(() => validation.value?.isValid ?? false)
+ }
+}
+\`\`\`
+
+## Testing Examples
+
+### Unit Tests
+
+\`\`\`javascript
+import { AnimationNameMapper } from '@kjanat/owen'
+
+describe('AnimationNameMapper', () => {
+ let mapper
+
+ beforeEach(() => {
+ mapper = new AnimationNameMapper()
+ })
+
+ test('converts legacy to semantic', () => {
+ expect(mapper.convert('wait_idle_L', 'semantic')).toBe('OwenWaitIdleLoop')
+ })
+
+ test('detects schemes correctly', () => {
+ expect(mapper.detectScheme('wait_idle_L')).toBe('legacy')
+ expect(mapper.detectScheme('Owen_ReactAngry')).toBe('artist')
+ expect(mapper.detectScheme('owen.state.sleep.peace.loop')).toBe('hierarchical')
+ expect(mapper.detectScheme('OwenTypeIdleLoop')).toBe('semantic')
+ })
+
+ test('validates animation names', () => {
+ const valid = mapper.validateAnimationName('wait_idle_L')
+ expect(valid.isValid).toBe(true)
+
+ const invalid = mapper.validateAnimationName('invalid_name')
+ expect(invalid.isValid).toBe(false)
+ expect(invalid.suggestions.length).toBeGreaterThan(0)
+ })
+
+ test('round-trip conversions', () => {
+ const original = 'wait_idle_L'
+ const semantic = mapper.convert(original, 'semantic')
+ const backToLegacy = mapper.convert(semantic, 'legacy')
+
+ expect(backToLegacy).toBe(original)
+ })
+})
+\`\`\`
+
+### Integration Tests
+
+\`\`\`javascript
+describe('OwenAnimationContext Multi-Scheme', () => {
+ let context
+
+ beforeEach(() => {
+ context = new OwenAnimationContext(mockGltf)
+ })
+
+ test('loads animations from all schemes', () => {
+ const schemes = [
+ { name: 'wait_idle_L', scheme: 'legacy' },
+ { name: 'Owen_WaitIdle', scheme: 'artist' },
+ { name: 'owen.state.wait.idle.loop', scheme: 'hierarchical' },
+ { name: 'OwenWaitIdleLoop', scheme: 'semantic' }
+ ]
+
+ schemes.forEach(({ name, scheme }) => {
+ expect(() => context.getClip(name)).not.toThrow()
+ })
+ })
+
+ test('equivalent animations return same clip', () => {
+ const legacy = context.getClip('wait_idle_L')
+ const semantic = context.getClip('OwenWaitIdleLoop')
+
+ // Should be the same underlying animation clip
+ expect(legacy).toBe(semantic)
+ })
+})
+\`\`\`
+
+This comprehensive examples document shows how to leverage the full power of the multi-scheme animation system in various scenarios and frameworks.
+`
+
+ const examplesDir = path.join(PROJECT_ROOT, 'examples')
+ if (!fs.existsSync(examplesDir)) {
+ fs.mkdirSync(examplesDir, { recursive: true })
+ }
+
+ fs.writeFileSync(path.join(examplesDir, 'ANIMATION_EXAMPLES.md'), content, 'utf8')
+ console.log('š Generated: examples/ANIMATION_EXAMPLES.md')
+}
+
+// Run the script if called directly
+if (process.argv[1] === __filename) {
+ generateAnimationDocs()
+ .catch(error => {
+ console.error('š„ Script failed:', error)
+ process.exit(1)
+ })
+}
diff --git a/scripts/generate-scheme-examples.js b/scripts/generate-scheme-examples.js
new file mode 100644
index 0000000..83f83b6
--- /dev/null
+++ b/scripts/generate-scheme-examples.js
@@ -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 (
+
+
Animation Player
+
+
+
+
+
Available Animations ({scheme} scheme)
+ {availableAnimations.map(anim => (
+
+ ))}
+
+
+
Currently playing: {currentAnimation}
+
+ )
+}
+\`\`\`
+
+## Vue.js Integration
+
+\`\`\`vue
+
+
+
Animation Controller
+
+
+
+
+
+
+
+
+
+
Current Animation in All Schemes:
+
+ {{ scheme }}: {{ name }}
+
+
+
+
+
+
+\`\`\`
+
+## 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)
+ })
+}
diff --git a/scripts/test-multi-schemes.js b/scripts/test-multi-schemes.js
new file mode 100644
index 0000000..9533007
--- /dev/null
+++ b/scripts/test-multi-schemes.js
@@ -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)
+ })
+}
diff --git a/scripts/validate-animations.js b/scripts/validate-animations.js
new file mode 100644
index 0000000..8b708fb
--- /dev/null
+++ b/scripts/validate-animations.js
@@ -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()
+}
diff --git a/scripts/validate-processed-animations.js b/scripts/validate-processed-animations.js
new file mode 100644
index 0000000..5128d57
--- /dev/null
+++ b/scripts/validate-processed-animations.js
@@ -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 }
diff --git a/src/animation/AnimationConstants.js b/src/animation/AnimationConstants.js
new file mode 100644
index 0000000..6b85bb5
--- /dev/null
+++ b/src/animation/AnimationConstants.js
@@ -0,0 +1,280 @@
+/**
+ * @fileoverview Animation constants with multi-scheme support for Owen Animation System
+ * @module animation/AnimationConstants
+ */
+
+import { AnimationNameMapper } from './AnimationNameMapper.js'
+
+// Create a singleton instance of the name mapper
+const nameMapper = new AnimationNameMapper()
+
+/**
+ * Legacy animation names (backward compatibility)
+ * @constant
+ */
+export const LegacyAnimations = {
+ // Wait state animations
+ WAIT_IDLE_LOOP: 'wait_idle_L',
+ WAIT_PICK_NOSE_QUIRK: 'wait_pickNose_Q',
+ WAIT_STRETCH_QUIRK: 'wait_stretch_Q',
+ WAIT_YAWN_QUIRK: 'wait_yawn_Q',
+
+ // React state animations - neutral
+ REACT_IDLE_LOOP: 'react_idle_L',
+ REACT_ACKNOWLEDGE_TRANSITION: 'react_acknowledge_T',
+ REACT_NOD_TRANSITION: 'react_nod_T',
+ REACT_LISTENING_LOOP: 'react_listening_L',
+
+ // React state animations - angry
+ REACT_ANGRY_IDLE_LOOP: 'react_angry_L',
+ REACT_ANGRY_FROWN_TRANSITION: 'react_an2frown_T',
+ REACT_ANGRY_GRUMBLE_QUIRK: 'react_an2grumble_Q',
+ REACT_ANGRY_TO_TYPE_TRANSITION: 'react_an2type_T',
+
+ // React state animations - happy
+ REACT_HAPPY_IDLE_LOOP: 'react_happy_L',
+ REACT_HAPPY_SMILE_TRANSITION: 'react_hp2smile_T',
+ REACT_HAPPY_BOUNCE_QUIRK: 'react_hp2bounce_Q',
+ REACT_HAPPY_TO_TYPE_TRANSITION: 'react_hp2type_T',
+
+ // React state animations - sad
+ REACT_SAD_IDLE_LOOP: 'react_sad_L',
+ REACT_SAD_SIGH_TRANSITION: 'react_sd2sigh_T',
+ REACT_SAD_SLUMP_QUIRK: 'react_sd2slump_Q',
+ REACT_SAD_TO_TYPE_TRANSITION: 'react_sd2type_T',
+
+ // React state animations - shocked
+ REACT_SHOCKED_IDLE_LOOP: 'react_shocked_L',
+ REACT_SHOCKED_GASP_TRANSITION: 'react_sh2gasp_T',
+ REACT_SHOCKED_JUMP_QUIRK: 'react_sh2jump_Q',
+ REACT_SHOCKED_TO_TYPE_TRANSITION: 'react_sh2type_T',
+
+ // Type state animations
+ TYPE_IDLE_LOOP: 'type_idle_L',
+ TYPE_FAST_LOOP: 'type_fast_L',
+ TYPE_SLOW_LOOP: 'type_slow_L',
+ TYPE_THINKING_LOOP: 'type_thinking_L',
+ TYPE_TO_WAIT_TRANSITION: 'type2wait_T',
+
+ // Sleep state animations
+ SLEEP_LIGHT_LOOP: 'sleep_light_L',
+ SLEEP_DEEP_LOOP: 'sleep_deep_L',
+ SLEEP_DREAM_QUIRK: 'sleep_dream_Q',
+ SLEEP_WAKE_UP_TRANSITION: 'sleep2wake_T'
+}
+
+/**
+ * Artist-friendly animation names (Blender workflow)
+ * @constant
+ */
+export const ArtistAnimations = {
+ // Wait state animations
+ WAIT_IDLE: 'Owen_WaitIdle',
+ WAIT_PICK_NOSE: 'Owen_PickNose',
+ WAIT_STRETCH: 'Owen_Stretch',
+ WAIT_YAWN: 'Owen_Yawn',
+
+ // React state animations - neutral
+ REACT_IDLE: 'Owen_ReactIdle',
+ REACT_ACKNOWLEDGE: 'Owen_ReactAcknowledge',
+ REACT_NOD: 'Owen_ReactNod',
+ REACT_LISTENING: 'Owen_ReactListening',
+
+ // React state animations - angry
+ REACT_ANGRY_IDLE: 'Owen_ReactAngryIdle',
+ REACT_ANGRY_FROWN: 'Owen_ReactAngryFrown',
+ REACT_ANGRY_GRUMBLE: 'Owen_ReactAngryGrumble',
+ REACT_ANGRY_TO_TYPE: 'Owen_ReactAngryToType',
+
+ // React state animations - happy
+ REACT_HAPPY_IDLE: 'Owen_ReactHappyIdle',
+ REACT_HAPPY_SMILE: 'Owen_ReactHappySmile',
+ REACT_HAPPY_BOUNCE: 'Owen_ReactHappyBounce',
+ REACT_HAPPY_TO_TYPE: 'Owen_ReactHappyToType',
+
+ // React state animations - sad
+ REACT_SAD_IDLE: 'Owen_ReactSadIdle',
+ REACT_SAD_SIGH: 'Owen_ReactSadSigh',
+ REACT_SAD_SLUMP: 'Owen_ReactSadSlump',
+ REACT_SAD_TO_TYPE: 'Owen_ReactSadToType',
+
+ // React state animations - shocked
+ REACT_SHOCKED_IDLE: 'Owen_ReactShockedIdle',
+ REACT_SHOCKED_GASP: 'Owen_ReactShockedGasp',
+ REACT_SHOCKED_JUMP: 'Owen_ReactShockedJump',
+ REACT_SHOCKED_TO_TYPE: 'Owen_ReactShockedToType',
+
+ // Type state animations
+ TYPE_IDLE: 'Owen_TypeIdle',
+ TYPE_FAST: 'Owen_TypeFast',
+ TYPE_SLOW: 'Owen_TypeSlow',
+ TYPE_THINKING: 'Owen_TypeThinking',
+ TYPE_TO_WAIT: 'Owen_TypeToWait',
+
+ // Sleep state animations
+ SLEEP_LIGHT: 'Owen_SleepLight',
+ SLEEP_DEEP: 'Owen_SleepDeep',
+ SLEEP_DREAM: 'Owen_SleepDream',
+ SLEEP_WAKE_UP: 'Owen_SleepWakeUp'
+}
+
+/**
+ * Hierarchical animation names (organized structure)
+ * @constant
+ */
+export const HierarchicalAnimations = {
+ // Wait state animations
+ WAIT_IDLE: 'owen.state.wait.idle.loop',
+ WAIT_PICK_NOSE: 'owen.quirk.wait.picknose',
+ WAIT_STRETCH: 'owen.quirk.wait.stretch',
+ WAIT_YAWN: 'owen.quirk.wait.yawn',
+
+ // React state animations - neutral
+ REACT_IDLE: 'owen.state.react.idle.loop',
+ REACT_ACKNOWLEDGE: 'owen.state.react.acknowledge.transition',
+ REACT_NOD: 'owen.state.react.nod.transition',
+ REACT_LISTENING: 'owen.state.react.listening.loop',
+
+ // React state animations - angry
+ REACT_ANGRY_IDLE: 'owen.state.react.angry.idle.loop',
+ REACT_ANGRY_FROWN: 'owen.state.react.angry.frown.transition',
+ REACT_ANGRY_GRUMBLE: 'owen.quirk.react.angry.grumble',
+ REACT_ANGRY_TO_TYPE: 'owen.state.react.angry.totype.transition',
+
+ // React state animations - happy
+ REACT_HAPPY_IDLE: 'owen.state.react.happy.idle.loop',
+ REACT_HAPPY_SMILE: 'owen.state.react.happy.smile.transition',
+ REACT_HAPPY_BOUNCE: 'owen.quirk.react.happy.bounce',
+ REACT_HAPPY_TO_TYPE: 'owen.state.react.happy.totype.transition',
+
+ // React state animations - sad
+ REACT_SAD_IDLE: 'owen.state.react.sad.idle.loop',
+ REACT_SAD_SIGH: 'owen.state.react.sad.sigh.transition',
+ REACT_SAD_SLUMP: 'owen.quirk.react.sad.slump',
+ REACT_SAD_TO_TYPE: 'owen.state.react.sad.totype.transition',
+
+ // React state animations - shocked
+ REACT_SHOCKED_IDLE: 'owen.state.react.shocked.idle.loop',
+ REACT_SHOCKED_GASP: 'owen.state.react.shocked.gasp.transition',
+ REACT_SHOCKED_JUMP: 'owen.quirk.react.shocked.jump',
+ REACT_SHOCKED_TO_TYPE: 'owen.state.react.shocked.totype.transition',
+
+ // Type state animations
+ TYPE_IDLE: 'owen.state.type.idle.loop',
+ TYPE_FAST: 'owen.state.type.fast.loop',
+ TYPE_SLOW: 'owen.state.type.slow.loop',
+ TYPE_THINKING: 'owen.state.type.thinking.loop',
+ TYPE_TO_WAIT: 'owen.state.type.towait.transition',
+
+ // Sleep state animations
+ SLEEP_LIGHT: 'owen.state.sleep.light.loop',
+ SLEEP_DEEP: 'owen.state.sleep.deep.loop',
+ SLEEP_DREAM: 'owen.quirk.sleep.dream',
+ SLEEP_WAKE_UP: 'owen.state.sleep.wakeup.transition'
+}
+
+/**
+ * Semantic animation names (readable camelCase)
+ * @constant
+ */
+export const SemanticAnimations = {
+ // Wait state animations
+ WAIT_IDLE: 'OwenWaitIdleLoop',
+ WAIT_PICK_NOSE: 'OwenQuirkPickNose',
+ WAIT_STRETCH: 'OwenQuirkStretch',
+ WAIT_YAWN: 'OwenQuirkYawn',
+
+ // React state animations - neutral
+ REACT_IDLE: 'OwenReactIdleLoop',
+ REACT_ACKNOWLEDGE: 'OwenReactAcknowledgeTransition',
+ REACT_NOD: 'OwenReactNodTransition',
+ REACT_LISTENING: 'OwenReactListeningLoop',
+
+ // React state animations - angry
+ REACT_ANGRY_IDLE: 'OwenReactAngryIdleLoop',
+ REACT_ANGRY_FROWN: 'OwenReactAngryFrownTransition',
+ REACT_ANGRY_GRUMBLE: 'OwenQuirkAngryGrumble',
+ REACT_ANGRY_TO_TYPE: 'OwenReactAngryToTypeTransition',
+
+ // React state animations - happy
+ REACT_HAPPY_IDLE: 'OwenReactHappyIdleLoop',
+ REACT_HAPPY_SMILE: 'OwenReactHappySmileTransition',
+ REACT_HAPPY_BOUNCE: 'OwenQuirkHappyBounce',
+ REACT_HAPPY_TO_TYPE: 'OwenReactHappyToTypeTransition',
+
+ // React state animations - sad
+ REACT_SAD_IDLE: 'OwenReactSadIdleLoop',
+ REACT_SAD_SIGH: 'OwenReactSadSighTransition',
+ REACT_SAD_SLUMP: 'OwenQuirkSadSlump',
+ REACT_SAD_TO_TYPE: 'OwenReactSadToTypeTransition',
+
+ // React state animations - shocked
+ REACT_SHOCKED_IDLE: 'OwenReactShockedIdleLoop',
+ REACT_SHOCKED_GASP: 'OwenReactShockedGaspTransition',
+ REACT_SHOCKED_JUMP: 'OwenQuirkShockedJump',
+ REACT_SHOCKED_TO_TYPE: 'OwenReactShockedToTypeTransition',
+
+ // Type state animations
+ TYPE_IDLE: 'OwenTypeIdleLoop',
+ TYPE_FAST: 'OwenTypeFastLoop',
+ TYPE_SLOW: 'OwenTypeSlowLoop',
+ TYPE_THINKING: 'OwenTypeThinkingLoop',
+ TYPE_TO_WAIT: 'OwenTypeToWaitTransition',
+
+ // Sleep state animations
+ SLEEP_LIGHT: 'OwenSleepLightLoop',
+ SLEEP_DEEP: 'OwenSleepDeepLoop',
+ SLEEP_DREAM: 'OwenQuirkSleepDream',
+ SLEEP_WAKE_UP: 'OwenSleepWakeUpTransition'
+}
+
+/**
+ * Animation naming schemes enumeration
+ * @constant
+ */
+export const NamingSchemes = {
+ LEGACY: 'legacy',
+ ARTIST: 'artist',
+ HIERARCHICAL: 'hierarchical',
+ SEMANTIC: 'semantic'
+}
+
+/**
+ * Convert animation name between different schemes
+ * @param {string} name - The source animation name
+ * @param {string} targetScheme - The target naming scheme
+ * @returns {string} The converted animation name
+ */
+export function convertAnimationName (name, targetScheme) {
+ return nameMapper.convert(name, targetScheme)
+}
+
+/**
+ * Get all naming scheme variants for an animation
+ * @param {string} name - The source animation name
+ * @returns {Object} Object with all scheme variants
+ */
+export function getAllAnimationNames (name) {
+ return nameMapper.getAllNames(name)
+}
+
+/**
+ * Validate an animation name
+ * @param {string} name - The animation name to validate
+ * @returns {Object} Validation result
+ */
+export function validateAnimationName (name) {
+ return nameMapper.validateAnimationName(name)
+}
+
+/**
+ * Get animations by state and emotion
+ * @param {string} state - The state name
+ * @param {string} emotion - The emotion name (optional)
+ * @param {string} scheme - The naming scheme to return (default: 'semantic')
+ * @returns {string[]} Array of animation names
+ */
+export function getAnimationsByStateAndEmotion (state, emotion = '', scheme = 'semantic') {
+ const animations = nameMapper.getAnimationsByFilter({ state, emotion })
+ return animations.map(anim => anim[scheme] || anim.semantic)
+}
diff --git a/src/animation/AnimationNameMapper.js b/src/animation/AnimationNameMapper.js
new file mode 100644
index 0000000..2585a34
--- /dev/null
+++ b/src/animation/AnimationNameMapper.js
@@ -0,0 +1,599 @@
+/**
+ * @fileoverview Multi-scheme animation name mapper for Owen Animation System
+ * @module animation/AnimationNameMapper
+ */
+
+/**
+ * Multi-scheme animation name mapper for Owen Animation System
+ * Supports legacy, artist-friendly, and hierarchical naming schemes
+ * @class
+ */
+export class AnimationNameMapper {
+ constructor () {
+ // Mapping between different naming schemes
+ this.schemeMappings = new Map()
+ this.reverseMappings = new Map()
+ this.patterns = new Map()
+
+ this.initializeMappings()
+ }
+
+ /**
+ * Initialize all naming scheme mappings and patterns
+ * @private
+ */
+ initializeMappings () {
+ // Core animation definitions with all naming scheme variants
+ const animations = [
+ // Wait state animations
+ {
+ legacy: 'wait_idle_L',
+ artist: 'Owen_WaitIdle',
+ hierarchical: 'owen.state.wait.idle.loop',
+ semantic: 'OwenWaitIdleLoop',
+ state: 'wait',
+ emotion: '',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'wait_pickNose_Q',
+ artist: 'Owen_PickNose',
+ hierarchical: 'owen.quirk.wait.picknose',
+ semantic: 'OwenQuirkPickNose',
+ state: 'wait',
+ emotion: '',
+ type: 'quirk',
+ category: 'quirk'
+ },
+ {
+ legacy: 'wait_wave_Q',
+ artist: 'Owen_Wave',
+ hierarchical: 'owen.quirk.wait.wave',
+ semantic: 'OwenQuirkWave',
+ state: 'wait',
+ emotion: '',
+ type: 'quirk',
+ category: 'quirk'
+ },
+ // React state animations
+ {
+ legacy: 'react_idle_L',
+ artist: 'Owen_ReactIdle',
+ hierarchical: 'owen.state.react.idle.loop',
+ semantic: 'OwenReactIdleLoop',
+ state: 'react',
+ emotion: '',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'react_an_L',
+ artist: 'Owen_ReactAngry',
+ hierarchical: 'owen.state.react.emotion.angry.loop',
+ semantic: 'OwenReactAngryLoop',
+ state: 'react',
+ emotion: 'angry',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'react_sh_L',
+ artist: 'Owen_ReactShocked',
+ hierarchical: 'owen.state.react.emotion.shocked.loop',
+ semantic: 'OwenReactShockedLoop',
+ state: 'react',
+ emotion: 'shocked',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'react_ha_L',
+ artist: 'Owen_ReactHappy',
+ hierarchical: 'owen.state.react.emotion.happy.loop',
+ semantic: 'OwenReactHappyLoop',
+ state: 'react',
+ emotion: 'happy',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'react_sa_L',
+ artist: 'Owen_ReactSad',
+ hierarchical: 'owen.state.react.emotion.sad.loop',
+ semantic: 'OwenReactSadLoop',
+ state: 'react',
+ emotion: 'sad',
+ type: 'loop',
+ category: 'state'
+ },
+ // Type state animations
+ {
+ legacy: 'type_idle_L',
+ artist: 'Owen_TypeIdle',
+ hierarchical: 'owen.state.type.idle.loop',
+ semantic: 'OwenTypeIdleLoop',
+ state: 'type',
+ emotion: '',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'type_an_L',
+ artist: 'Owen_TypeAngry',
+ hierarchical: 'owen.state.type.emotion.angry.loop',
+ semantic: 'OwenTypeAngryLoop',
+ state: 'type',
+ emotion: 'angry',
+ type: 'loop',
+ category: 'state'
+ },
+ {
+ legacy: 'type_sh_L',
+ artist: 'Owen_TypeShocked',
+ hierarchical: 'owen.state.type.emotion.shocked.loop',
+ semantic: 'OwenTypeShockedLoop',
+ state: 'type',
+ emotion: 'shocked',
+ type: 'loop',
+ category: 'state'
+ },
+ // Sleep state animations
+ {
+ legacy: 'sleep_idle_L',
+ artist: 'Owen_SleepIdle',
+ hierarchical: 'owen.state.sleep.idle.loop',
+ semantic: 'OwenSleepIdleLoop',
+ state: 'sleep',
+ emotion: '',
+ type: 'loop',
+ category: 'state'
+ },
+ // Transition animations
+ {
+ legacy: 'wait_2react_T',
+ artist: 'Owen_WaitToReact',
+ hierarchical: 'owen.transition.wait.to.react',
+ semantic: 'OwenTransitionWaitToReact',
+ fromState: 'wait',
+ toState: 'react',
+ emotion: '',
+ type: 'transition',
+ category: 'transition'
+ },
+ {
+ legacy: 'react_2type_T',
+ artist: 'Owen_ReactToType',
+ hierarchical: 'owen.transition.react.to.type',
+ semantic: 'OwenTransitionReactToType',
+ fromState: 'react',
+ toState: 'type',
+ emotion: '',
+ type: 'transition',
+ category: 'transition'
+ },
+ {
+ legacy: 'react_an2type_T',
+ artist: 'Owen_ReactAngryToType',
+ hierarchical: 'owen.transition.react.to.type.emotion.angry',
+ semantic: 'OwenTransitionReactToTypeAngry',
+ fromState: 'react',
+ toState: 'type',
+ emotion: 'angry',
+ type: 'transition',
+ category: 'transition'
+ },
+ {
+ legacy: 'type_2wait_T',
+ artist: 'Owen_TypeToWait',
+ hierarchical: 'owen.transition.type.to.wait',
+ semantic: 'OwenTransitionTypeToWait',
+ fromState: 'type',
+ toState: 'wait',
+ emotion: '',
+ type: 'transition',
+ category: 'transition'
+ },
+ {
+ legacy: 'sleep_2wait_T',
+ artist: 'Owen_SleepToWait',
+ hierarchical: 'owen.transition.sleep.to.wait',
+ semantic: 'OwenTransitionSleepToWait',
+ fromState: 'sleep',
+ toState: 'wait',
+ emotion: '',
+ type: 'transition',
+ category: 'transition'
+ }
+ ]
+
+ // Build bidirectional mappings
+ animations.forEach(anim => {
+ const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
+
+ schemes.forEach(scheme1 => {
+ schemes.forEach(scheme2 => {
+ if (scheme1 !== scheme2) {
+ this.schemeMappings.set(anim[scheme1], anim[scheme2])
+ }
+ })
+
+ // Also map to animation definition
+ this.schemeMappings.set(anim[scheme1], anim)
+ })
+ })
+
+ // Initialize pattern matchers for auto-detection
+ this.initializePatterns()
+ }
+
+ /**
+ * Initialize pattern matchers for different naming schemes
+ * @private
+ */
+ initializePatterns () {
+ // Pattern matchers for different naming schemes
+ this.patterns.set('legacy', [
+ {
+ regex: /^(\w+)_(\w+)_([LTQ])$/,
+ extract: (match) => ({
+ state: match[1],
+ action: match[2],
+ type: match[3] === 'L' ? 'loop' : match[3] === 'T' ? 'transition' : 'quirk'
+ })
+ },
+ {
+ regex: /^(\w+)_(\w{2})_([LTQ])$/,
+ extract: (match) => ({
+ state: match[1],
+ emotion: this.mapEmotionCode(match[2]),
+ type: match[3] === 'L' ? 'loop' : match[3] === 'T' ? 'transition' : 'quirk'
+ })
+ },
+ {
+ regex: /^(\w+)_(\w{2})?2(\w+)_T$/,
+ extract: (match) => ({
+ fromState: match[1],
+ emotion: match[2] ? this.mapEmotionCode(match[2]) : '',
+ toState: match[3],
+ type: 'transition'
+ })
+ },
+ {
+ regex: /^(\w+)_2(\w+)_T$/,
+ extract: (match) => ({
+ fromState: match[1],
+ toState: match[2],
+ type: 'transition'
+ })
+ }
+ ])
+
+ this.patterns.set('artist', [
+ {
+ regex: /^Owen_(\w+)$/,
+ extract: (match) => ({
+ action: match[1],
+ scheme: 'artist'
+ })
+ },
+ {
+ regex: /^Owen_(\w+)To(\w+)$/,
+ extract: (match) => ({
+ fromState: match[1].toLowerCase(),
+ toState: match[2].toLowerCase(),
+ type: 'transition'
+ })
+ },
+ {
+ regex: /^Owen_(\w+)(Angry|Happy|Sad|Shocked)$/,
+ extract: (match) => ({
+ state: match[1].toLowerCase(),
+ emotion: match[2].toLowerCase(),
+ type: 'loop'
+ })
+ },
+ {
+ regex: /^Owen_(\w+)(Angry|Happy|Sad|Shocked)To(\w+)$/,
+ extract: (match) => ({
+ fromState: match[1].toLowerCase(),
+ emotion: match[2].toLowerCase(),
+ toState: match[3].toLowerCase(),
+ type: 'transition'
+ })
+ }
+ ])
+
+ this.patterns.set('hierarchical', [
+ {
+ regex: /^owen\.(\w+)\.(\w+)\.(\w+)(?:\.(\w+))?(?:\.(\w+))?$/,
+ extract: (match) => ({
+ category: match[1],
+ subcategory: match[2],
+ action: match[3],
+ modifier: match[4],
+ type: match[5] || match[4]
+ })
+ }
+ ])
+
+ this.patterns.set('semantic', [
+ {
+ regex: /^Owen(\w+)(\w+)(\w+)$/,
+ extract: (match) => ({
+ category: match[1].toLowerCase(),
+ action: match[2].toLowerCase(),
+ type: match[3].toLowerCase()
+ })
+ }
+ ])
+ }
+
+ /**
+ * Map emotion codes to full names
+ * @private
+ * @param {string} code - Emotion code
+ * @returns {string} Full emotion name
+ */
+ mapEmotionCode (code) {
+ const emotionMap = {
+ an: 'angry',
+ sh: 'shocked',
+ ha: 'happy',
+ sa: 'sad',
+ '': 'neutral'
+ }
+ return emotionMap[code] || code
+ }
+
+ /**
+ * Convert any animation name to any other scheme
+ * @param {string} fromName - Source animation name
+ * @param {string} targetScheme - Target naming scheme ('legacy', 'artist', 'hierarchical', 'semantic')
+ * @returns {string} Converted animation name
+ */
+ convert (fromName, targetScheme = 'hierarchical') {
+ // Direct lookup first
+ const directMapping = this.schemeMappings.get(fromName)
+ if (directMapping && typeof directMapping === 'object') {
+ return directMapping[targetScheme] || fromName
+ }
+
+ // Pattern-based conversion
+ const detected = this.detectScheme(fromName)
+ if (detected) {
+ return this.generateName(detected.info, targetScheme)
+ }
+
+ console.warn(`Could not convert animation name: ${fromName}`)
+ return fromName
+ }
+
+ /**
+ * Detect which naming scheme is being used
+ * @param {string} name - Animation name to analyze
+ * @returns {Object|null} Detection result with scheme and extracted info
+ */
+ detectScheme (name) {
+ for (const [scheme, patterns] of this.patterns) {
+ for (const pattern of patterns) {
+ const match = name.match(pattern.regex)
+ if (match) {
+ return {
+ scheme,
+ info: pattern.extract(match),
+ originalName: name
+ }
+ }
+ }
+ }
+ return null
+ }
+
+ /**
+ * Generate animation name in target scheme
+ * @private
+ * @param {Object} info - Animation information
+ * @param {string} targetScheme - Target naming scheme
+ * @returns {string} Generated animation name
+ */
+ generateName (info, targetScheme) {
+ switch (targetScheme) {
+ case 'legacy':
+ return this.generateLegacyName(info)
+ case 'artist':
+ return this.generateArtistName(info)
+ case 'hierarchical':
+ return this.generateHierarchicalName(info)
+ case 'semantic':
+ return this.generateSemanticName(info)
+ default:
+ return null
+ }
+ }
+
+ /**
+ * Generate legacy format name
+ * @private
+ * @param {Object} info - Animation information
+ * @returns {string} Legacy format name
+ */
+ generateLegacyName (info) {
+ const typeMap = { loop: 'L', transition: 'T', quirk: 'Q' }
+ const emotionMap = { angry: 'an', shocked: 'sh', happy: 'ha', sad: 'sa' }
+
+ if (info.type === 'transition' && info.fromState && info.toState) {
+ const emotionPart = info.emotion ? emotionMap[info.emotion] || '' : ''
+ return emotionPart
+ ? `${info.fromState}_${emotionPart}2${info.toState}_T`
+ : `${info.fromState}_2${info.toState}_T`
+ }
+
+ const state = info.state || info.fromState || 'wait'
+ const action = info.action || (info.emotion ? emotionMap[info.emotion] : 'idle')
+ const type = typeMap[info.type] || 'L'
+
+ return `${state}_${action}_${type}`
+ }
+
+ /**
+ * Generate artist-friendly format name
+ * @private
+ * @param {Object} info - Animation information
+ * @returns {string} Artist format name
+ */
+ generateArtistName (info) {
+ const parts = ['Owen']
+
+ if (info.type === 'transition') {
+ const from = this.capitalize(info.fromState || info.state)
+ const to = this.capitalize(info.toState)
+ if (info.emotion) {
+ parts.push(`${from}${this.capitalize(info.emotion)}To${to}`)
+ } else {
+ parts.push(`${from}To${to}`)
+ }
+ } else {
+ if (info.state) parts.push(this.capitalize(info.state))
+ if (info.action && info.action !== 'idle') parts.push(this.capitalize(info.action))
+ if (info.emotion) parts.push(this.capitalize(info.emotion))
+ }
+
+ return parts.join('_')
+ }
+
+ /**
+ * Generate hierarchical format name
+ * @private
+ * @param {Object} info - Animation information
+ * @returns {string} Hierarchical format name
+ */
+ generateHierarchicalName (info) {
+ const parts = ['owen']
+
+ if (info.category) {
+ parts.push(info.category)
+ } else if (info.type === 'transition') {
+ parts.push('transition')
+ } else if (info.type === 'quirk') {
+ parts.push('quirk')
+ } else {
+ parts.push('state')
+ }
+
+ if (info.fromState && info.toState) {
+ // Transition
+ parts.push(info.fromState, 'to', info.toState)
+ } else if (info.state) {
+ parts.push(info.state)
+ }
+
+ if (info.action && info.action !== 'idle') parts.push(info.action)
+ if (info.emotion) parts.push('emotion', info.emotion)
+ if (info.type) parts.push(info.type)
+
+ return parts.join('.')
+ }
+
+ /**
+ * Generate semantic format name
+ * @private
+ * @param {Object} info - Animation information
+ * @returns {string} Semantic format name
+ */
+ generateSemanticName (info) {
+ const parts = ['Owen']
+
+ if (info.type === 'transition') {
+ parts.push('Transition')
+ if (info.fromState) parts.push(this.capitalize(info.fromState))
+ parts.push('To')
+ if (info.toState) parts.push(this.capitalize(info.toState))
+ if (info.emotion) parts.push(this.capitalize(info.emotion))
+ } else {
+ if (info.type === 'quirk') parts.push('Quirk')
+ if (info.state) parts.push(this.capitalize(info.state))
+ if (info.action && info.action !== 'idle') parts.push(this.capitalize(info.action))
+ if (info.emotion) parts.push(this.capitalize(info.emotion))
+ if (info.type && info.type !== 'quirk') parts.push(this.capitalize(info.type))
+ }
+
+ return parts.join('')
+ }
+
+ /**
+ * Capitalize first letter of string
+ * @private
+ * @param {string} str - String to capitalize
+ * @returns {string} Capitalized string
+ */
+ capitalize (str) {
+ return str.charAt(0).toUpperCase() + str.slice(1)
+ }
+
+ /**
+ * Get all possible names for an animation
+ * @param {string} animationName - Source animation name
+ * @returns {Object} Object with all naming scheme variants
+ */
+ getAllNames (animationName) {
+ const schemes = ['legacy', 'artist', 'hierarchical', 'semantic']
+ const names = {}
+
+ schemes.forEach(scheme => {
+ names[scheme] = this.convert(animationName, scheme)
+ })
+
+ return names
+ }
+
+ /**
+ * Batch convert multiple animations
+ * @param {string[]} animations - Array of animation names
+ * @param {string} targetScheme - Target naming scheme
+ * @returns {Object} Mapping of original names to converted names
+ */
+ convertBatch (animations, targetScheme) {
+ const converted = {}
+ animations.forEach(name => {
+ converted[name] = this.convert(name, targetScheme)
+ })
+ return converted
+ }
+
+ /**
+ * Validate animation name format
+ * @param {string} name - Animation name to validate
+ * @returns {Object} Validation result with issues and suggestions
+ */
+ validateAnimationName (name) {
+ const issues = []
+ const suggestions = []
+
+ // Check for common issues
+ if (name.includes(' ')) {
+ issues.push(`ā "${name}" contains spaces - may cause issues`)
+ suggestions.push(`š” Suggestion: "${name.replace(/ /g, '_')}"`)
+ }
+
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
+ issues.push(`ā "${name}" contains invalid characters`)
+ suggestions.push('š” Use only letters, numbers, dots, underscores, and hyphens')
+ }
+
+ if (name.length > 50) {
+ issues.push(`ā ļø "${name}" is very long (${name.length} chars)`)
+ suggestions.push('š” Consider shortening the name')
+ }
+
+ const detected = this.detectScheme(name)
+ if (!detected) {
+ issues.push(`ā ļø "${name}" doesn't match any known naming pattern`)
+ suggestions.push('š” Consider using one of: legacy, artist, hierarchical, or semantic format')
+ } else {
+ suggestions.push(`ā
Detected as ${detected.scheme} scheme`)
+ }
+
+ return { issues, suggestions, detected }
+ }
+}
diff --git a/src/constants.js b/src/constants.js
index a8eed72..011ffe6 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -32,13 +32,13 @@ export const ClipTypes = {
*/
export const States = {
/** Waiting/idle state */
- WAITING: 'wait',
+ WAITING: 'wait',
/** Reacting to input state */
- REACTING: 'react',
+ REACTING: 'react',
/** Typing response state */
- TYPING: 'type',
+ TYPING: 'type',
/** Sleep/inactive state */
- SLEEPING: 'sleep'
+ SLEEPING: 'sleep'
}
/**
diff --git a/src/core/OwenAnimationContext.js b/src/core/OwenAnimationContext.js
index cb2a530..0f5a12c 100644
--- a/src/core/OwenAnimationContext.js
+++ b/src/core/OwenAnimationContext.js
@@ -4,6 +4,7 @@
*/
import { States, Emotions, Config } from '../constants.js'
+import { AnimationNameMapper } from '../animation/AnimationNameMapper.js'
/**
* Main controller for the Owen animation system
@@ -43,6 +44,12 @@ export class OwenAnimationContext {
*/
this.stateFactory = stateFactory
+ /**
+ * Multi-scheme animation name mapper
+ * @type {AnimationNameMapper}
+ */
+ this.nameMapper = new AnimationNameMapper()
+
/**
* Map of animation clips by name
* @type {Map}
@@ -59,7 +66,7 @@ export class OwenAnimationContext {
* Current active state
* @type {string}
*/
- this.currentState = States.WAITING
+ this.currentState = States.WAITING
/**
* Current active state handler
@@ -105,7 +112,7 @@ export class OwenAnimationContext {
this.initializeStates()
// Start in wait state
- await this.transitionTo(States.WAITING)
+ await this.transitionTo(States.WAITING)
this.initialized = true
console.log('Owen Animation System initialized')
@@ -167,8 +174,8 @@ export class OwenAnimationContext {
this.onUserActivity()
// If sleeping, wake up first
- if (this.currentState === States.SLEEPING) {
- await this.transitionTo(States.REACTING)
+ if (this.currentState === States.SLEEPING) {
+ await this.transitionTo(States.REACTING)
}
// Let current state handle the message
@@ -177,10 +184,10 @@ export class OwenAnimationContext {
}
// Transition to appropriate next state based on current state
- if (this.currentState === States.WAITING) {
- await this.transitionTo(States.REACTING);
- } else if (this.currentState === States.REACTING) {
- await this.transitionTo(States.TYPING)
+ if (this.currentState === States.WAITING) {
+ await this.transitionTo(States.REACTING)
+ } else if (this.currentState === States.REACTING) {
+ await this.transitionTo(States.TYPING)
}
}
@@ -192,8 +199,8 @@ export class OwenAnimationContext {
this.resetActivityTimer()
// Wake up if sleeping
- if (this.currentState === States.SLEEPING) {
- this.transitionTo(States.WAITING)
+ if (this.currentState === States.SLEEPING) {
+ this.transitionTo(States.WAITING)
}
}
@@ -213,7 +220,7 @@ export class OwenAnimationContext {
*/
async handleInactivity () {
console.log('Inactivity detected, transitioning to sleep')
- await this.transitionTo(States.SLEEPING)
+ await this.transitionTo(States.SLEEPING)
}
/**
@@ -234,18 +241,38 @@ export class OwenAnimationContext {
// Update inactivity timer
this.inactivityTimer += deltaTime
- if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEPING) {
+ if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEPING) {
this.handleInactivity()
}
}
/**
- * Get an animation clip by name
- * @param {string} name - The animation clip name
+ * Get an animation clip by name (supports all naming schemes)
+ * @param {string} name - The animation clip name in any supported scheme
* @returns {AnimationClip|undefined} The animation clip or undefined if not found
*/
getClip (name) {
- return this.clips.get(name)
+ // First try direct lookup
+ let clip = this.clips.get(name)
+ if (clip) return clip
+
+ // Try to find clip using name mapper
+ try {
+ const allNames = this.nameMapper.getAllNames(name)
+
+ // Try each possible name variant
+ for (const variant of Object.values(allNames)) {
+ clip = this.clips.get(variant)
+ if (clip) return clip
+ }
+ } catch (error) {
+ // If name mapping fails, continue with pattern search
+ console.debug(`Name mapping failed for "${name}":`, error.message)
+ }
+
+ // Fall back to pattern matching for legacy compatibility
+ const exactMatches = this.getClipsByPattern(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`)
+ return exactMatches.length > 0 ? exactMatches[0] : undefined
}
/**
@@ -266,6 +293,80 @@ export class OwenAnimationContext {
return matches
}
+ /**
+ * Get an animation clip by name in a specific naming scheme
+ * @param {string} name - The animation name
+ * @param {string} [targetScheme] - Target scheme: 'legacy', 'artist', 'hierarchical', 'semantic'
+ * @returns {AnimationClip|undefined} The animation clip or undefined if not found
+ */
+ getClipByScheme (name, targetScheme) {
+ try {
+ if (targetScheme) {
+ const convertedName = this.nameMapper.convert(name, targetScheme)
+ return this.clips.get(convertedName)
+ } else {
+ return this.getClip(name)
+ }
+ } catch (error) {
+ console.debug(`Scheme conversion failed for "${name}" to "${targetScheme}":`, error.message)
+ return undefined
+ }
+ }
+
+ /**
+ * Get all naming scheme variants for an animation
+ * @param {string} name - The animation name in any scheme
+ * @returns {Object} Object with all scheme variants: {legacy, artist, hierarchical, semantic}
+ */
+ getAnimationNames (name) {
+ try {
+ return this.nameMapper.getAllNames(name)
+ } catch (error) {
+ console.warn(`Could not get animation name variants for "${name}":`, error.message)
+ return {
+ legacy: name,
+ artist: name,
+ hierarchical: name,
+ semantic: name
+ }
+ }
+ }
+
+ /**
+ * Validate an animation name and get suggestions if invalid
+ * @param {string} name - The animation name to validate
+ * @returns {Object} Validation result with isValid, scheme, error, and suggestions
+ */
+ validateAnimationName (name) {
+ try {
+ return this.nameMapper.validateAnimationName(name)
+ } catch (error) {
+ return {
+ isValid: false,
+ scheme: 'unknown',
+ error: error.message,
+ suggestions: []
+ }
+ }
+ }
+
+ /**
+ * Get available animations by state and emotion
+ * @param {string} state - The state name (wait, react, type, sleep)
+ * @param {string} [emotion] - The emotion name (angry, happy, sad, shocked, neutral)
+ * @param {string} [scheme='semantic'] - The naming scheme to return
+ * @returns {string[]} Array of animation names in the specified scheme
+ */
+ getAnimationsByStateAndEmotion (state, emotion = '', scheme = 'semantic') {
+ try {
+ const animations = this.nameMapper.getAnimationsByFilter({ state, emotion })
+ return animations.map(anim => anim[scheme] || anim.semantic)
+ } catch (error) {
+ console.warn(`Could not filter animations by state "${state}" and emotion "${emotion}":`, error.message)
+ return []
+ }
+ }
+
/**
* Get the current state name
* @returns {string} The current state name
diff --git a/src/index.js b/src/index.js
index 83ffcb8..84990f6 100644
--- a/src/index.js
+++ b/src/index.js
@@ -14,6 +14,20 @@ export { OwenAnimationContext } from './core/OwenAnimationContext.js'
// Animation system exports
export { AnimationClip, AnimationClipFactory } from './animation/AnimationClip.js'
+// Multi-scheme animation naming exports
+export { AnimationNameMapper } from './animation/AnimationNameMapper.js'
+export {
+ LegacyAnimations,
+ ArtistAnimations,
+ HierarchicalAnimations,
+ SemanticAnimations,
+ NamingSchemes,
+ convertAnimationName,
+ getAllAnimationNames,
+ validateAnimationName,
+ getAnimationsByStateAndEmotion
+} from './animation/AnimationConstants.js'
+
// Loader exports
export { AnimationLoader, GLTFAnimationLoader } from './loaders/AnimationLoader.js'
diff --git a/src/states/ReactStateHandler.js b/src/states/ReactStateHandler.js
index 6289d7a..f024aad 100644
--- a/src/states/ReactStateHandler.js
+++ b/src/states/ReactStateHandler.js
@@ -17,7 +17,7 @@ export class ReactStateHandler extends StateHandler {
* @param {OwenAnimationContext} context - The animation context
*/
constructor (context) {
- super(States.REACTING, context)
+ super(States.REACTING, context)
/**
* Current emotional state
@@ -33,7 +33,7 @@ export class ReactStateHandler extends StateHandler {
* @returns {Promise}
*/
async enter (_fromState = null, emotion = Emotions.NEUTRAL) {
- console.log(`Entering REACTING state with emotion: ${emotion}`)
+ console.log(`Entering REACTING state with emotion: ${emotion}`)
this.emotion = emotion
// Play appropriate reaction
@@ -51,7 +51,7 @@ export class ReactStateHandler extends StateHandler {
* @returns {Promise}
*/
async exit (toState = null, emotion = Emotions.NEUTRAL) {
- console.log(`Exiting REACTING state to ${toState} with emotion: ${emotion}`)
+ console.log(`Exiting REACTING state to ${toState} with emotion: ${emotion}`)
if (this.currentClip) {
await this.stopCurrentClip()
@@ -154,6 +154,6 @@ export class ReactStateHandler extends StateHandler {
* @returns {string[]} Array of available state transitions
*/
getAvailableTransitions () {
- return [ States.TYPING, States.WAITING ]
+ return [States.TYPING, States.WAITING]
}
}
diff --git a/src/states/SleepStateHandler.js b/src/states/SleepStateHandler.js
index 0c0818a..42e30ec 100644
--- a/src/states/SleepStateHandler.js
+++ b/src/states/SleepStateHandler.js
@@ -17,7 +17,7 @@ export class SleepStateHandler extends StateHandler {
* @param {OwenAnimationContext} context - The animation context
*/
constructor (context) {
- super(States.SLEEPING, context)
+ super(States.SLEEPING, context)
/**
* Sleep animation clip
@@ -39,7 +39,7 @@ export class SleepStateHandler extends StateHandler {
* @returns {Promise}
*/
async enter (fromState = null, _emotion = Emotions.NEUTRAL) {
- console.log(`Entering SLEEPING state from ${fromState}`)
+ console.log(`Entering SLEEPING state from ${fromState}`)
// Play sleep transition if available
const sleepTransition = this.context.getClip('wait_2sleep_T')
@@ -65,7 +65,7 @@ export class SleepStateHandler extends StateHandler {
* @returns {Promise}
*/
async exit (toState = null, _emotion = Emotions.NEUTRAL) {
- console.log(`Exiting SLEEPING state to ${toState}`)
+ console.log(`Exiting SLEEPING state to ${toState}`)
this.isDeepSleep = false
if (this.currentClip) {
@@ -107,8 +107,8 @@ export class SleepStateHandler extends StateHandler {
// Any message should wake up the character
if (this.isDeepSleep) {
console.log('Waking up due to user message')
- // This will trigger a state transition to REACTING
- await this.context.transitionTo(States.REACTING)
+ // This will trigger a state transition to REACTING
+ await this.context.transitionTo(States.REACTING)
}
}
@@ -117,7 +117,7 @@ export class SleepStateHandler extends StateHandler {
* @returns {string[]} Array of available state transitions
*/
getAvailableTransitions () {
- return [ States.WAITING, States.REACTING ]
+ return [States.WAITING, States.REACTING]
}
/**
@@ -134,7 +134,7 @@ export class SleepStateHandler extends StateHandler {
*/
async wakeUp () {
if (this.isDeepSleep) {
- await this.context.transitionTo(States.WAITING)
+ await this.context.transitionTo(States.WAITING)
}
}
}
diff --git a/src/states/StateFactory.js b/src/states/StateFactory.js
index 669b431..ab8c778 100644
--- a/src/states/StateFactory.js
+++ b/src/states/StateFactory.js
@@ -26,10 +26,10 @@ export class StateFactory {
this.stateHandlers = new Map()
// Register default state handlers
- this.registerStateHandler(States.WAITING, WaitStateHandler);
- this.registerStateHandler(States.REACTING, ReactStateHandler);
- this.registerStateHandler(States.TYPING, TypeStateHandler);
- this.registerStateHandler(States.SLEEPING, SleepStateHandler)
+ this.registerStateHandler(States.WAITING, WaitStateHandler)
+ this.registerStateHandler(States.REACTING, ReactStateHandler)
+ this.registerStateHandler(States.TYPING, TypeStateHandler)
+ this.registerStateHandler(States.SLEEPING, SleepStateHandler)
}
/**
diff --git a/src/states/TypeStateHandler.js b/src/states/TypeStateHandler.js
index f85e61e..1175761 100644
--- a/src/states/TypeStateHandler.js
+++ b/src/states/TypeStateHandler.js
@@ -17,7 +17,7 @@ export class TypeStateHandler extends StateHandler {
* @param {OwenAnimationContext} context - The animation context
*/
constructor (context) {
- super(States.TYPING, context)
+ super(States.TYPING, context)
/**
* Current emotional state
@@ -39,7 +39,7 @@ export class TypeStateHandler extends StateHandler {
* @returns {Promise}
*/
async enter (_fromState = null, emotion = Emotions.NEUTRAL) {
- console.log(`Entering TYPING state with emotion: ${emotion}`)
+ console.log(`Entering TYPING state with emotion: ${emotion}`)
this.emotion = emotion
this.isTyping = true
@@ -63,7 +63,7 @@ export class TypeStateHandler extends StateHandler {
* @returns {Promise}
*/
async exit (toState = null, _emotion = Emotions.NEUTRAL) {
- console.log(`Exiting TYPING state to ${toState}`)
+ console.log(`Exiting TYPING state to ${toState}`)
this.isTyping = false
if (this.currentClip) {
@@ -106,7 +106,7 @@ export class TypeStateHandler extends StateHandler {
* @returns {string[]} Array of available state transitions
*/
getAvailableTransitions () {
- return [ States.WAITING, States.REACTING ]
+ return [States.WAITING, States.REACTING]
}
/**
diff --git a/src/states/WaitStateHandler.js b/src/states/WaitStateHandler.js
index 84a5aac..91623ce 100644
--- a/src/states/WaitStateHandler.js
+++ b/src/states/WaitStateHandler.js
@@ -17,7 +17,7 @@ export class WaitStateHandler extends StateHandler {
* @param {OwenAnimationContext} context - The animation context
*/
constructor (context) {
- super(States.WAITING, context)
+ super(States.WAITING, context)
/**
* The main idle animation clip
@@ -51,7 +51,7 @@ export class WaitStateHandler extends StateHandler {
* @returns {Promise}
*/
async enter (fromState = null, _emotion = Emotions.NEUTRAL) {
- console.log(`Entering WAITING state from ${fromState}`)
+ console.log(`Entering WAITING state from ${fromState}`)
// Play idle loop
this.idleClip = this.context.getClip('wait_idle_L')
@@ -72,7 +72,7 @@ export class WaitStateHandler extends StateHandler {
* @returns {Promise}
*/
async exit (toState = null, _emotion = Emotions.NEUTRAL) {
- console.log(`Exiting WAITING state to ${toState}`)
+ console.log(`Exiting WAITING state to ${toState}`)
if (this.currentClip) {
await this.stopCurrentClip()
@@ -133,6 +133,6 @@ export class WaitStateHandler extends StateHandler {
* @returns {string[]} Array of available state transitions
*/
getAvailableTransitions () {
- return [ States.REACTING, States.SLEEPING ]
+ return [States.REACTING, States.SLEEPING]
}
}
diff --git a/tests/demo.spec.js b/tests/demo.spec.js
new file mode 100644
index 0000000..c512089
--- /dev/null
+++ b/tests/demo.spec.js
@@ -0,0 +1,158 @@
+import { test, expect } from '@playwright/test'
+
+test.describe('Owen Animation System Demo', () => {
+ test.beforeEach(async ({ page }) => {
+ // Navigate to the demo page before each test
+ await page.goto('/')
+ })
+
+ test('should load the main demo page', async ({ page }) => {
+ // Check that the page title is correct
+ await expect(page).toHaveTitle(/Owen Animation System/)
+
+ // Check that the main heading is present
+ await expect(page.locator('h1')).toContainText('Owen Animation System')
+
+ // Check that the demo content is loaded
+ await expect(page.locator('.demo-content')).toBeVisible()
+ })
+
+ test('should display animation name converter', async ({ page }) => {
+ // Check that the converter section is present
+ await expect(page.locator('.converter-section')).toBeVisible()
+
+ // Check input fields
+ await expect(page.locator('#animationName')).toBeVisible()
+ await expect(page.locator('#sourceScheme')).toBeVisible()
+ await expect(page.locator('#targetScheme')).toBeVisible()
+
+ // Check convert button
+ await expect(page.locator('#convertBtn')).toBeVisible()
+ })
+
+ test('should convert animation names', async ({ page }) => {
+ // Fill in the converter form
+ await page.fill('#animationName', 'char_walk_01')
+ await page.selectOption('#sourceScheme', 'artist')
+ await page.selectOption('#targetScheme', 'semantic')
+
+ // Click convert button
+ await page.click('#convertBtn')
+
+ // Check that result is displayed
+ await expect(page.locator('#conversionResult')).toBeVisible()
+ await expect(page.locator('#conversionResult')).toContainText('character.movement.walk')
+ })
+
+ test('should validate animation names', async ({ page }) => {
+ // Test with invalid animation name
+ await page.fill('#animationName', 'invalid-name-123!@#')
+ await page.selectOption('#sourceScheme', 'semantic')
+ await page.click('#convertBtn')
+
+ // Should show validation error
+ await expect(page.locator('.error-message')).toBeVisible()
+
+ // Test with valid animation name
+ await page.fill('#animationName', 'character.idle.basic')
+ await page.click('#convertBtn')
+
+ // Should show success
+ await expect(page.locator('.success-message')).toBeVisible()
+ })
+
+ test('should show scheme comparison', async ({ page }) => {
+ // Check that scheme cards are present
+ await expect(page.locator('.scheme-card')).toHaveCount(4)
+
+ // Check that each scheme is represented
+ await expect(page.locator('.scheme-card')).toContainText(['Legacy', 'Artist', 'Hierarchical', 'Semantic'])
+ })
+
+ test('should handle batch conversion', async ({ page }) => {
+ // Click on batch conversion tab
+ await page.click('[data-tab="batch"]')
+
+ // Fill in batch input
+ const batchInput = [
+ 'char_walk_01',
+ 'char_run_02',
+ 'prop_door_open'
+ ].join('\n')
+
+ await page.fill('#batchInput', batchInput)
+ await page.selectOption('#batchSourceScheme', 'artist')
+ await page.selectOption('#batchTargetScheme', 'semantic')
+
+ // Click convert batch button
+ await page.click('#convertBatchBtn')
+
+ // Check that results are displayed
+ await expect(page.locator('#batchResults')).toBeVisible()
+ await expect(page.locator('.batch-result-item')).toHaveCount(3)
+ })
+
+ test('should export results', async ({ page }) => {
+ // Convert some animation names first
+ await page.fill('#animationName', 'char_walk_01')
+ await page.selectOption('#sourceScheme', 'artist')
+ await page.selectOption('#targetScheme', 'semantic')
+ await page.click('#convertBtn')
+
+ // Wait for result
+ await expect(page.locator('#conversionResult')).toBeVisible()
+
+ // Click export button
+ const downloadPromise = page.waitForEvent('download')
+ await page.click('#exportBtn')
+ const download = await downloadPromise
+
+ // Check that file was downloaded
+ expect(download.suggestedFilename()).toMatch(/animation-conversions.*\.json/)
+ })
+
+ test('should be responsive', async ({ page }) => {
+ // Test mobile viewport
+ await page.setViewportSize({ width: 375, height: 667 })
+
+ // Check that mobile navigation is present
+ await expect(page.locator('.mobile-nav')).toBeVisible()
+
+ // Check that converter still works
+ await page.fill('#animationName', 'test_animation')
+ await page.selectOption('#sourceScheme', 'legacy')
+ await page.selectOption('#targetScheme', 'semantic')
+ await page.click('#convertBtn')
+
+ await expect(page.locator('#conversionResult')).toBeVisible()
+ })
+
+ test('should handle errors gracefully', async ({ page }) => {
+ // Test with empty input
+ await page.click('#convertBtn')
+ await expect(page.locator('.error-message')).toContainText('Animation name is required')
+
+ // Test with same source and target scheme
+ await page.fill('#animationName', 'test_animation')
+ await page.selectOption('#sourceScheme', 'semantic')
+ await page.selectOption('#targetScheme', 'semantic')
+ await page.click('#convertBtn')
+
+ await expect(page.locator('.warning-message')).toContainText('Source and target schemes are the same')
+ })
+
+ test('should show performance metrics', async ({ page }) => {
+ // Check that performance section is present
+ await expect(page.locator('.performance-section')).toBeVisible()
+
+ // Convert some animations to generate metrics
+ await page.fill('#animationName', 'char_walk_01')
+ await page.selectOption('#sourceScheme', 'artist')
+ await page.selectOption('#targetScheme', 'semantic')
+ await page.click('#convertBtn')
+
+ // Check that metrics are updated
+ await expect(page.locator('.conversion-time')).toContainText(/\d+ms/)
+ await expect(page.locator('.total-conversions')).toContainText(/\d+/)
+ })
+})
diff --git a/tests/pages.spec.js b/tests/pages.spec.js
new file mode 100644
index 0000000..41d9cdc
--- /dev/null
+++ b/tests/pages.spec.js
@@ -0,0 +1,177 @@
+import { test, expect } from '@playwright/test'
+
+test.describe('Examples Page', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/examples.html')
+ })
+
+ test('should load examples page', async ({ page }) => {
+ await expect(page).toHaveTitle(/Examples/)
+ await expect(page.locator('h1')).toContainText('Integration Examples')
+ })
+
+ test('should display framework examples', async ({ page }) => {
+ // Check that example cards are present
+ await expect(page.locator('.example-card')).toHaveCount.greaterThan(3)
+
+ // Check specific framework examples
+ await expect(page.locator('.example-card')).toContainText(['React', 'Vue', 'Node.js'])
+ })
+
+ test('should copy code examples', async ({ page }) => {
+ // Click on a copy button
+ await page.click('.copy-button').first()
+
+ // Check that button text changes to "Copied!"
+ await expect(page.locator('.copy-button').first()).toContainText('Copied!')
+ })
+
+ test('should filter examples', async ({ page }) => {
+ // Click on React filter
+ await page.click('[data-filter="react"]')
+
+ // Check that only React examples are shown
+ await expect(page.locator('.example-card:visible')).toHaveCount(1)
+ await expect(page.locator('.example-card:visible')).toContainText('React')
+ })
+})
+
+test.describe('Comparison Page', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/comparison.html')
+ })
+
+ test('should load comparison page', async ({ page }) => {
+ await expect(page).toHaveTitle(/Comparison/)
+ await expect(page.locator('h1')).toContainText('Naming Scheme Comparison')
+ })
+
+ test('should display scheme cards', async ({ page }) => {
+ await expect(page.locator('.scheme-card')).toHaveCount(4)
+
+ // Check each scheme is present
+ const schemes = ['Legacy', 'Artist', 'Hierarchical', 'Semantic']
+ for (const scheme of schemes) {
+ await expect(page.locator('.scheme-card')).toContainText(scheme)
+ }
+ })
+
+ test('should show comparison table', async ({ page }) => {
+ await expect(page.locator('.comparison-table')).toBeVisible()
+
+ // Check table headers
+ await expect(page.locator('.comparison-table th')).toContainText(['Animation Name', 'Legacy', 'Artist', 'Hierarchical', 'Semantic'])
+ })
+
+ test('should filter comparison table', async ({ page }) => {
+ // Type in search box
+ await page.fill('.search-input', 'walk')
+
+ // Check that results are filtered
+ await expect(page.locator('.comparison-table tbody tr:visible')).toHaveCount.greaterThan(0)
+ await expect(page.locator('.comparison-table tbody tr:visible')).toContainText('walk')
+ })
+
+ test('should convert between schemes', async ({ page }) => {
+ // Use the conversion demo
+ await page.fill('.animation-input', 'char_walk_01')
+ await page.selectOption('#sourceSchemeSelect', 'artist')
+ await page.selectOption('#targetSchemeSelect', 'semantic')
+
+ // Check conversion result
+ await expect(page.locator('.conversion-result')).toBeVisible()
+ await expect(page.locator('.result-value')).toContainText('character.movement.walk')
+ })
+})
+
+test.describe('Interactive Playground', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/interactive.html')
+ })
+
+ test('should load interactive page', async ({ page }) => {
+ await expect(page).toHaveTitle(/Interactive/)
+ await expect(page.locator('h1')).toContainText('Interactive Playground')
+ })
+
+ test('should have functional controls', async ({ page }) => {
+ // Check that control sections are present
+ await expect(page.locator('.playground-controls')).toBeVisible()
+ await expect(page.locator('.playground-main')).toBeVisible()
+ await expect(page.locator('.performance-monitor')).toBeVisible()
+ })
+
+ test('should switch between tabs', async ({ page }) => {
+ // Click on different tabs
+ await page.click('[data-tab="converter"]')
+ await expect(page.locator('.tab-content[data-tab="converter"]')).toBeVisible()
+
+ await page.click('[data-tab="validator"]')
+ await expect(page.locator('.tab-content[data-tab="validator"]')).toBeVisible()
+
+ await page.click('[data-tab="generator"]')
+ await expect(page.locator('.tab-content[data-tab="generator"]')).toBeVisible()
+ })
+
+ test('should run code in playground', async ({ page }) => {
+ // Switch to code editor tab
+ await page.click('[data-tab="code"]')
+
+ // Clear and enter new code
+ await page.fill('.code-editor', `
+const mapper = new AnimationNameMapper();
+const result = mapper.convert('char_walk_01', 'artist', 'semantic');
+console.log(result);
+ `)
+
+ // Run the code
+ await page.click('#runCodeBtn')
+
+ // Check output
+ await expect(page.locator('.output-panel')).toContainText('character.movement.walk')
+ })
+
+ test('should validate animation names in real-time', async ({ page }) => {
+ // Enter valid animation name
+ await page.fill('#playgroundAnimationName', 'character.idle.basic')
+ await page.selectOption('#playgroundScheme', 'semantic')
+
+ // Check validation indicator
+ await expect(page.locator('.validation-indicator')).toHaveClass(/success/)
+
+ // Enter invalid animation name
+ await page.fill('#playgroundAnimationName', 'invalid-name-123!')
+
+ // Check validation indicator shows error
+ await expect(page.locator('.validation-indicator')).toHaveClass(/error/)
+ })
+
+ test('should show performance metrics', async ({ page }) => {
+ // Perform some conversions
+ await page.fill('#playgroundAnimationName', 'char_walk_01')
+ await page.selectOption('#playgroundSourceScheme', 'artist')
+ await page.selectOption('#playgroundTargetScheme', 'semantic')
+ await page.click('#convertPlaygroundBtn')
+
+ // Check that performance metrics are updated
+ await expect(page.locator('.monitor-value')).toHaveCount.greaterThan(0)
+ await expect(page.locator('.conversion-time .monitor-value')).toContainText(/\d+/)
+ })
+
+ test('should save and load history', async ({ page }) => {
+ // Perform a conversion
+ await page.fill('#playgroundAnimationName', 'test_animation')
+ await page.selectOption('#playgroundSourceScheme', 'legacy')
+ await page.selectOption('#playgroundTargetScheme', 'semantic')
+ await page.click('#convertPlaygroundBtn')
+
+ // Check that history is updated
+ await expect(page.locator('.history-item')).toHaveCount.greaterThan(0)
+
+ // Click on history item to load it
+ await page.click('.history-item').first()
+
+ // Check that form is populated
+ await expect(page.locator('#playgroundAnimationName')).toHaveValue('test_animation')
+ })
+})
diff --git a/vite.demo.config.js b/vite.demo.config.js
new file mode 100644
index 0000000..3fca3c4
--- /dev/null
+++ b/vite.demo.config.js
@@ -0,0 +1,167 @@
+import { defineConfig } from 'vite'
+import path from 'path'
+
+/**
+ * Vite configuration for Owen Animation System Demo
+ *
+ * This configuration builds the demo application showcasing
+ * the multi-scheme animation naming system and its features.
+ */
+export default defineConfig({
+ // Demo-specific build configuration
+ root: './demo',
+
+ build: {
+ outDir: '../dist-demo',
+ emptyOutDir: true,
+
+ // Optimization settings for demo
+ rollupOptions: {
+ input: {
+ main: path.resolve(__dirname, 'demo/index.html'),
+ examples: path.resolve(__dirname, 'demo/examples.html'),
+ comparison: path.resolve(__dirname, 'demo/comparison.html'),
+ interactive: path.resolve(__dirname, 'demo/interactive.html')
+ },
+
+ output: {
+ // Asset organization
+ assetFileNames: (assetInfo) => {
+ const info = assetInfo.name.split('.')
+ const ext = info[info.length - 1]
+
+ if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(ext)) {
+ return 'assets/images/[name]-[hash][extname]'
+ }
+
+ if (/woff2?|eot|ttf|otf/i.test(ext)) {
+ return 'assets/fonts/[name]-[hash][extname]'
+ }
+
+ if (/gltf|glb|fbx/i.test(ext)) {
+ return 'assets/animations/[name]-[hash][extname]'
+ }
+
+ return 'assets/[name]-[hash][extname]'
+ },
+
+ chunkFileNames: 'js/[name]-[hash].js',
+ entryFileNames: 'js/[name]-[hash].js'
+ }
+ },
+
+ // Source maps for debugging
+ sourcemap: true,
+
+ // Minification
+ minify: 'terser',
+ terserOptions: {
+ compress: {
+ drop_console: false, // Keep console logs for demo
+ drop_debugger: true
+ }
+ },
+
+ // Target modern browsers for demo
+ target: 'es2020'
+ },
+
+ // Development server settings
+ server: {
+ port: 3001,
+ host: true,
+ open: '/demo/',
+
+ // Proxy API calls if needed
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, '')
+ }
+ }
+ },
+
+ // Preview server settings
+ preview: {
+ port: 3002,
+ host: true,
+ open: true
+ },
+
+ // Resolve configuration
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ '@demo': path.resolve(__dirname, 'demo'),
+ '@assets': path.resolve(__dirname, 'assets'),
+ '@examples': path.resolve(__dirname, 'examples')
+ }
+ },
+
+ // Define global constants for demo
+ define: {
+ __DEMO_VERSION__: JSON.stringify(process.env.npm_package_version || '1.0.0'),
+ __BUILD_TIMESTAMP__: JSON.stringify(new Date().toISOString()),
+ __ANIMATION_SCHEMES__: JSON.stringify(['legacy', 'artist', 'hierarchical', 'semantic'])
+ },
+
+ // Plugin configuration
+ plugins: [
+ // Add any demo-specific plugins here
+ ],
+
+ // CSS configuration
+ css: {
+ preprocessorOptions: {
+ scss: {
+ additionalData: `
+ @import "@demo/styles/variables.scss";
+ @import "@demo/styles/mixins.scss";
+ `
+ }
+ },
+
+ // CSS modules for component styling
+ modules: {
+ localsConvention: 'camelCase'
+ }
+ },
+
+ // Asset handling
+ assetsInclude: [
+ '**/*.gltf',
+ '**/*.glb',
+ '**/*.fbx',
+ '**/*.babylon'
+ ],
+
+ // Optimization
+ optimizeDeps: {
+ include: [
+ 'three',
+ 'three/examples/jsm/loaders/GLTFLoader',
+ 'three/examples/jsm/loaders/FBXLoader',
+ 'three/examples/jsm/controls/OrbitControls'
+ ],
+ exclude: [
+ // Exclude any demo-specific modules that shouldn't be pre-bundled
+ ]
+ },
+
+ // Environment variables
+ envPrefix: 'OWEN_DEMO_',
+
+ // Base path for deployment
+ base: process.env.NODE_ENV === 'production' ? '/Owen/' : '/',
+
+ // Worker configuration for animation processing
+ worker: {
+ format: 'es'
+ },
+
+ // Experimental features
+ experimental: {
+ buildAdvancedBaseOptions: true
+ }
+})