"""Autonomous Asset Compiler for FlyBrain V8/V9. Implements Sections 29, 32–36, 48: - Normalizes visual mesh into calibrated physical meters - Generates low-overhead physics collision proxy (Box / Capsule / Hull) - Emits schema_version 1 FlyAsset JSON package - Content-addressed hashing avoiding redundant generation """ import os import json import hashlib import numpy as np from typing import Dict, Any, List, Optional from src.assets.compiler.types import FlyAsset, AssetClass, ColliderType, CollisionProxy, REFERENCE_SCALES_M class AssetCompiler: def __init__(self, output_dir: str = "assets/compiled"): self.output_dir = os.path.abspath(output_dir) os.makedirs(self.output_dir, exist_ok=True) def compute_asset_id(self, semantic_name: str, input_hash: str, params: Dict[str, Any]) -> str: """Section 48: Content-addressed asset identifier.""" data = f"{semantic_name}_{input_hash}_{json.dumps(params, sort_keys=True)}" return hashlib.sha256(data.encode("utf-8")).hexdigest()[:16] def compile_asset(self, semantic_name: str, category: AssetClass, visual_mesh_path: str, generator_info: str = "procedural_builder", input_hash: str = "seed42", custom_dimensions: Optional[List[float]] = None, explicit_asset_id: Optional[str] = None) -> FlyAsset: """Compiles, scale-normalizes, and builds collision proxies for an asset.""" if explicit_asset_id: full_asset_id = explicit_asset_id else: asset_id = self.compute_asset_id(semantic_name, input_hash, {"cat": category.value}) full_asset_id = f"{semantic_name}_{asset_id}" # 1. Physical Scale Normalization (Section 34) if custom_dimensions: target_dims = [float(x) for x in custom_dimensions] elif semantic_name.lower() in REFERENCE_SCALES_M: target_dims = REFERENCE_SCALES_M[semantic_name.lower()] else: # Default heuristic based on category if category == AssetClass.STRUCTURE: target_dims = [5.0, 5.0, 3.0] elif category == AssetClass.VEGETATION: target_dims = [2.0, 2.0, 4.0] elif category == AssetClass.CREATURE: target_dims = [0.5, 0.3, 0.2] else: target_dims = [1.0, 1.0, 1.0] # 2. Collision Proxy Generation (Section 35) # Dedicated box or capsule collider if category in (AssetClass.STRUCTURE, AssetClass.STATIC): coll_type = ColliderType.BOX elif category in (AssetClass.CHARACTER, AssetClass.CREATURE): coll_type = ColliderType.CAPSULE else: coll_type = ColliderType.BOX density = 500.0 if category == AssetClass.VEGETATION else 1200.0 volume_m3 = target_dims[0] * target_dims[1] * target_dims[2] computed_mass = round(float(density * volume_m3), 2) collider = CollisionProxy( type=coll_type, dimensions_m=target_dims, offset_m=[0.0, 0.0, target_dims[2] / 2.0], mass_kg=max(0.1, computed_mass), friction=[0.8, 0.1, 0.01], restitution=0.05 ) # 3. Create Package asset = FlyAsset( schema_version=1, asset_id=full_asset_id, asset_class=category, source_hash=input_hash, generator=generator_info, visual_mesh=visual_mesh_path, dimensions_m=target_dims, collision={ "type": collider.type.value, "dimensions_m": collider.dimensions_m, "offset_m": collider.offset_m, "mass_kg": collider.mass_kg, "friction": collider.friction, "restitution": collider.restitution }, physics={ "mass": collider.mass_kg, "dynamic": category in (AssetClass.RIGID_DYNAMIC, AssetClass.CHARACTER, AssetClass.CREATURE) }, provenance={ "created_at": os.path.getmtime(visual_mesh_path) if os.path.exists(visual_mesh_path) else None, "semantic_name": semantic_name, "scale_normalization": "APPLIED_PHYSICAL_METERS" } ) # Save package manifest pkg_file = os.path.join(self.output_dir, f"{asset.asset_id}.json") with open(pkg_file, "w", encoding="utf-8") as f: json.dump(asset.to_dict(), f, indent=2) return asset