From 79cdd4a1325084d6ed7ca3e6d97d36799d016587 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Wed, 19 Aug 2026 13:03:06 +0000 Subject: [PATCH] WIP checkpoint: US-1: Asset Import Pipeline \u2014 glTF upload endpoint with metadata extraction --- backend/app/routers/assets.py | 77 +++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/backend/app/routers/assets.py b/backend/app/routers/assets.py index af9c2be..423f20c 100644 --- a/backend/app/routers/assets.py +++ b/backend/app/routers/assets.py @@ -37,6 +37,7 @@ class AssetResponse(BaseModel): file_path: str file_size: Optional[int] mime_type: Optional[str] + metadata_json: Optional[dict] world_id: Optional[int] owner_id: int is_public: bool @@ -64,6 +65,75 @@ def _sanitize_filename(filename: str) -> str: return safe or "file" +def _extract_gltf_metadata(content: bytes) -> Optional[dict]: + """Extract metadata from a glTF/GLB file. + + Returns a dict with mesh_count, vertex_count, has_animation, texture_refs + if the file is a valid glTF/GLB, or None otherwise. + """ + try: + import gltflib + except ImportError: + return None + + try: + model = gltflib.GLFModel.from_json(content) + except Exception: + return None + + # Count meshes + meshes = model.meshes or [] + mesh_count = len(meshes) + + # Count total vertices across all primitives + vertex_count = 0 + for mesh in meshes: + primitives = mesh.primitives or [] + for primitive in primitives: + indices = primitive.indices + if indices is not None: + # Use index count + accessor = model.accessors[indices] if indices < len(model.accessors or []) else None + if accessor and accessor.count is not None: + vertex_count += accessor.count + else: + vertex_count += 1 # fallback + else: + # No indices — count from POSITION attribute + if primitive.attributes and primitive.attributes.POSITION is not None: + accessor = model.accessors[primitive.attributes.POSITION] if primitive.attributes.POSITION < len(model.accessors or []) else None + if accessor and accessor.count is not None: + vertex_count += accessor.count + else: + vertex_count += 1 # fallback + + # Check for animations + animations = model.animations or [] + has_animation = len(animations) > 0 + + # Collect texture references + texture_refs = [] + textures = model.textures or [] + for texture in textures: + if texture.source is not None and texture.source < len(model.images or []): + image = model.images[texture.source] + ref = {} + if image.name: + ref["name"] = image.name + if image.uri: + ref["uri"] = image.uri + if image.mimeType: + ref["mime_type"] = image.mimeType + texture_refs.append(ref) + + return { + "mesh_count": mesh_count, + "vertex_count": vertex_count, + "has_animation": has_animation, + "texture_refs": texture_refs, + } + + # --- Endpoints --- @router.post("/", response_model=AssetResponse, status_code=201) @@ -204,6 +274,13 @@ async def upload_asset_file( asset.mime_type = file.content_type asset.file_size = file_size + # Extract glTF metadata for glTF/GLB files. + filename_lower = (file.filename or "").lower() + if filename_lower.endswith(".glb") or filename_lower.endswith(".gltf"): + metadata = _extract_gltf_metadata(content) + if metadata is not None: + asset.metadata_json = str(metadata) + await db.commit() await db.refresh(asset) return asset