feat: GPU rendering + material matching + perf improvements

- GPU: fix Cycles device activation order — set compute_device_type
  BEFORE engine init, re-set AFTER open_mainfile wipes preferences
- GPU: remove _mark_sharp_and_seams edit-mode loop (redundant with
  Blender 5.0 shade_smooth_by_angle), saves ~200s/render on 175 parts
- Material: fix _AFN suffix mismatch — build AF-stripped mat_map keys
  and add prefix fallback in _apply_material_library (blender_render.py)
- Material: production GLB now uses get_material_library_path() which
  checks active AssetLibrary instead of empty legacy system setting
- Admin: RenderTemplateTable multi-select output types (M2M frontend)
- Admin: MaterialLibraryPanel replaced with link to Asset Libraries
- UX: move Toaster to top-left to avoid dispatch button overlap
- SQLAlchemy: add .unique() to all RenderTemplate M2M collection queries
- Logging: flush=True on all Blender progress prints, stdout reconfigure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 19:05:03 +01:00
parent 934728da77
commit ee6eb34b4c
34 changed files with 1274 additions and 511 deletions
+56 -16
View File
@@ -35,8 +35,10 @@ class RenderTemplateOut(BaseModel):
id: str
name: str
category_key: str | None
output_type_id: str | None
output_type_name: str | None
output_type_id: str | None # legacy single FK
output_type_name: str | None # legacy
output_type_ids: list[str] # M2M
output_type_names: list[str] # M2M display names
blend_file_path: str
original_filename: str
target_collection: str
@@ -54,7 +56,7 @@ class RenderTemplateOut(BaseModel):
class RenderTemplateUpdate(BaseModel):
name: str | None = None
category_key: str | None = None
output_type_id: str | None = None
output_type_ids: list[str] | None = None # replaces output_type_id
target_collection: str | None = None
material_replace_enabled: bool | None = None
lighting_only: bool | None = None
@@ -74,12 +76,17 @@ def _to_out(t: RenderTemplate) -> dict:
ot_name = None
if t.output_type:
ot_name = t.output_type.name
# M2M output types
ot_ids = [str(ot.id) for ot in t.output_types] if t.output_types else []
ot_names = [ot.name for ot in t.output_types] if t.output_types else []
return {
"id": str(t.id),
"name": t.name,
"category_key": t.category_key,
"output_type_id": str(t.output_type_id) if t.output_type_id else None,
"output_type_name": ot_name,
"output_type_ids": ot_ids,
"output_type_names": ot_names,
"blend_file_path": t.blend_file_path,
"original_filename": t.original_filename,
"target_collection": t.target_collection,
@@ -103,7 +110,7 @@ async def list_render_templates(
result = await db.execute(
select(RenderTemplate).order_by(RenderTemplate.created_at.desc())
)
return [_to_out(t) for t in result.scalars().all()]
return [_to_out(t) for t in result.unique().scalars().all()]
@router.post("/render-templates", response_model=RenderTemplateOut, status_code=status.HTTP_201_CREATED)
@@ -151,6 +158,17 @@ async def create_render_template(
camera_orbit=camera_orbit,
)
db.add(tmpl)
await db.flush()
# Sync M2M from initial output_type_id
if ot_uuid:
from app.domains.rendering.models import render_template_output_types
await db.execute(
render_template_output_types.insert().values(
template_id=template_id, output_type_id=ot_uuid,
)
)
await db.commit()
await db.refresh(tmpl)
@@ -170,7 +188,7 @@ async def update_render_template(
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(RenderTemplate).where(RenderTemplate.id == template_id))
tmpl = result.scalar_one_or_none()
tmpl = result.unique().scalar_one_or_none()
if not tmpl:
raise HTTPException(404, detail="Render template not found")
@@ -179,12 +197,9 @@ async def update_render_template(
# Normalise empty strings to None for nullable fields
if "category_key" in updates and updates["category_key"] in ("", "null"):
updates["category_key"] = None
if "output_type_id" in updates:
val = updates["output_type_id"]
if val in ("", "null", None):
updates["output_type_id"] = None
else:
updates["output_type_id"] = uuid.UUID(val)
# Handle M2M output_type_ids
new_ot_ids: list[str] | None = updates.pop("output_type_ids", None)
if updates:
updates["updated_at"] = datetime.utcnow()
@@ -193,9 +208,34 @@ async def update_render_template(
.where(RenderTemplate.id == template_id)
.values(**updates)
)
await db.commit()
await db.refresh(tmpl)
# Sync M2M relationship
if new_ot_ids is not None:
from app.domains.rendering.models import render_template_output_types
# Delete existing links
await db.execute(
sql_delete(render_template_output_types).where(
render_template_output_types.c.template_id == template_id
)
)
# Insert new links
for ot_id in new_ot_ids:
await db.execute(
render_template_output_types.insert().values(
template_id=template_id,
output_type_id=uuid.UUID(ot_id),
)
)
# Also update legacy FK to first OT (for backward compat)
legacy_ot = uuid.UUID(new_ot_ids[0]) if new_ot_ids else None
await db.execute(
sql_update(RenderTemplate)
.where(RenderTemplate.id == template_id)
.values(output_type_id=legacy_ot, updated_at=datetime.utcnow())
)
await db.commit()
await db.refresh(tmpl)
return _to_out(tmpl)
@@ -206,7 +246,7 @@ async def delete_render_template(
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(RenderTemplate).where(RenderTemplate.id == template_id))
tmpl = result.scalar_one_or_none()
tmpl = result.unique().scalar_one_or_none()
if not tmpl:
raise HTTPException(404, detail="Render template not found")
@@ -231,7 +271,7 @@ async def upload_blend_file(
raise HTTPException(400, detail="File must be a .blend file")
result = await db.execute(select(RenderTemplate).where(RenderTemplate.id == template_id))
tmpl = result.scalar_one_or_none()
tmpl = result.unique().scalar_one_or_none()
if not tmpl:
raise HTTPException(404, detail="Render template not found")
@@ -266,7 +306,7 @@ async def download_blend_file(
db: AsyncSession = Depends(get_db),
):
result = await db.execute(select(RenderTemplate).where(RenderTemplate.id == template_id))
tmpl = result.scalar_one_or_none()
tmpl = result.unique().scalar_one_or_none()
if not tmpl:
raise HTTPException(404, detail="Render template not found")