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:
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user