feat(PBR): extract Blender PBR properties and apply in 3D viewer
Extract Base Color, Metallic, Roughness, Transmission, IOR from Blender asset library materials via catalog_assets.py. Store in catalog JSON and serve via /api/asset-libraries/pbr-map endpoint. Frontend viewers apply PBR properties to Three.js MeshStandardMaterial using hex color strings (avoiding Three.js ColorManagement sRGB/linear issues). Key fixes: - RLS bypass for material alias lookup in pbr-map endpoint - pbrMap empty guard prevents premature grey fallback in viewers - Cache-Control: no-cache on pbr-map requests to avoid stale data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,13 +4,15 @@ import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.database import get_db
|
||||
from sqlalchemy import text
|
||||
from app.config import settings
|
||||
from app.domains.materials.models import AssetLibrary
|
||||
from app.domains.materials.models import AssetLibrary, Material, MaterialAlias
|
||||
from app.utils.auth import require_admin_or_pm
|
||||
|
||||
router = APIRouter(prefix="/asset-libraries", tags=["asset-libraries"])
|
||||
@@ -58,6 +60,50 @@ def _to_out(lib: AssetLibrary) -> dict:
|
||||
|
||||
# ── Endpoints ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/pbr-map")
|
||||
async def get_material_pbr_map(db: AsyncSession = Depends(get_db)):
|
||||
"""PBR properties for all materials in the active asset library.
|
||||
|
||||
Public (no auth) — needed by all 3D viewers.
|
||||
Returns keyed by canonical name AND all known aliases so the viewer can
|
||||
look up materials by raw Excel names (e.g. "Steel--Stahl") without needing
|
||||
a separate alias resolution step.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(AssetLibrary).where(AssetLibrary.is_active == True).limit(1) # noqa: E712
|
||||
)
|
||||
lib = result.scalar_one_or_none()
|
||||
if not lib or not lib.catalog:
|
||||
return JSONResponse(content={}, headers={"Cache-Control": "public, max-age=3600"})
|
||||
materials = lib.catalog.get("materials", [])
|
||||
pbr_map: dict = {}
|
||||
for m in materials:
|
||||
if isinstance(m, str):
|
||||
continue # old format — skip
|
||||
pbr_map[m["name"]] = {
|
||||
"base_color": m.get("base_color", [0.5, 0.5, 0.5]),
|
||||
"metallic": m.get("metallic", 0.0),
|
||||
"roughness": m.get("roughness", 0.5),
|
||||
"transmission": m.get("transmission", 0.0),
|
||||
"ior": m.get("ior", 1.45),
|
||||
}
|
||||
|
||||
# Also index by aliases so frontend can look up by raw Excel names
|
||||
# (e.g. "Steel--Stahl" → same PBR as "SCHAEFFLER_010101_Steel-Bare")
|
||||
# Bypass RLS — this is public data and aliases may have NULL tenant_id
|
||||
if pbr_map:
|
||||
await db.execute(text("SET LOCAL app.current_tenant_id = 'bypass'"))
|
||||
alias_result = await db.execute(
|
||||
select(MaterialAlias.alias, Material.name)
|
||||
.join(Material, MaterialAlias.material_id == Material.id)
|
||||
)
|
||||
for alias, canonical_name in alias_result.all():
|
||||
if canonical_name in pbr_map and alias not in pbr_map:
|
||||
pbr_map[alias] = pbr_map[canonical_name]
|
||||
|
||||
return JSONResponse(content=pbr_map, headers={"Cache-Control": "public, max-age=3600"})
|
||||
|
||||
|
||||
@router.get("", response_model=list[AssetLibraryOut])
|
||||
async def list_asset_libraries(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
|
||||
Reference in New Issue
Block a user