feat(K): Blender Asset Library + production exports (GLB + .blend)

- feat(migration): 045_asset_libraries — new asset_libraries table (blend_file_path, catalog JSONB)
- feat(model): AssetLibrary SQLAlchemy model in domains/materials/models.py
- feat(api): POST/GET/PATCH/DELETE /api/asset-libraries + /upload-blend + /refresh-catalog endpoints
- feat(celery): refresh_asset_library_catalog task on thumbnail_rendering queue — runs Blender headless
- feat(blender): catalog_assets.py — extracts asset-marked materials + node_groups from .blend
- feat(blender): asset_library.py — apply_asset_library_materials + apply_asset_library_node_groups helpers
- feat(blender): export_gltf.py — STEP→STL→GLB production export with optional asset library
- feat(blender): export_blend.py — STEP→STL→.blend production export with pack_all()
- feat(frontend): api/assetLibraries.ts — full CRUD API client
- feat(frontend): AssetLibraryPanel in Admin.tsx — upload, refresh, expand catalog view
- docs: Blender asset_data marking requirement learning in LEARNINGS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 20:56:26 +01:00
parent 7a1329958d
commit a18d4c23ec
14 changed files with 922 additions and 10 deletions
+60
View File
@@ -0,0 +1,60 @@
import api from './client'
export interface AssetLibraryCatalog {
materials: string[]
node_groups: string[]
}
export interface AssetLibrary {
id: string
name: string
description: string | null
original_filename: string | null
catalog: AssetLibraryCatalog
is_active: boolean
created_at: string
updated_at: string
}
export async function listAssetLibraries(): Promise<AssetLibrary[]> {
const { data } = await api.get<AssetLibrary[]>('/asset-libraries')
return data
}
export async function getAssetLibrary(id: string): Promise<AssetLibrary> {
const { data } = await api.get<AssetLibrary>(`/asset-libraries/${id}`)
return data
}
export async function createAssetLibrary(params: {
name: string
description?: string
blend_file: File
}): Promise<AssetLibrary> {
const form = new FormData()
form.append('name', params.name)
if (params.description) form.append('description', params.description)
form.append('blend_file', params.blend_file)
const { data } = await api.post<AssetLibrary>('/asset-libraries', form, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return data
}
export async function uploadAssetLibraryBlend(id: string, file: File): Promise<AssetLibrary> {
const form = new FormData()
form.append('blend_file', file)
const { data } = await api.post<AssetLibrary>(`/asset-libraries/${id}/upload-blend`, form, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return data
}
export async function refreshAssetLibraryCatalog(id: string): Promise<AssetLibrary> {
const { data } = await api.post<AssetLibrary>(`/asset-libraries/${id}/refresh-catalog`)
return data
}
export async function deleteAssetLibrary(id: string): Promise<void> {
await api.delete(`/asset-libraries/${id}`)
}