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:
@@ -4,19 +4,20 @@ Used from Celery tasks (sync context) to find the best matching .blend template
|
||||
for a given category + output type combination.
|
||||
|
||||
Cascade priority (first active match wins):
|
||||
1. Exact: category_key + output_type_id
|
||||
2. Category only: category_key + output_type_id IS NULL
|
||||
3. OT only: category_key IS NULL + output_type_id
|
||||
4. Global: both NULL
|
||||
1. Exact: category_key + output_type linked via M2M
|
||||
2. Category only: category_key + no output_types linked
|
||||
3. OT only: category_key IS NULL + output_type linked via M2M
|
||||
4. Global: category_key IS NULL + no output_types linked
|
||||
5. No template → caller falls back to factory-settings behavior
|
||||
"""
|
||||
import logging
|
||||
|
||||
from sqlalchemy import create_engine, select, and_
|
||||
from sqlalchemy import create_engine, select, and_, exists
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.render_template import RenderTemplate
|
||||
from app.models.system_setting import SystemSetting
|
||||
from app.domains.rendering.models import render_template_output_types
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,63 +38,92 @@ def resolve_template(
|
||||
) -> RenderTemplate | None:
|
||||
"""Find the best matching active render template.
|
||||
|
||||
Uses the M2M render_template_output_types table for output type matching.
|
||||
Uses sync SQLAlchemy — safe for Celery tasks.
|
||||
"""
|
||||
engine = _get_engine()
|
||||
with Session(engine) as session:
|
||||
active = RenderTemplate.is_active == True # noqa: E712
|
||||
|
||||
# 1. Exact match
|
||||
# Helper: subquery checking if a template is linked to a specific OT
|
||||
def _has_ot(ot_id):
|
||||
return exists(
|
||||
select(render_template_output_types.c.template_id).where(and_(
|
||||
render_template_output_types.c.template_id == RenderTemplate.id,
|
||||
render_template_output_types.c.output_type_id == ot_id,
|
||||
))
|
||||
)
|
||||
|
||||
# Helper: subquery checking if a template has NO linked OTs
|
||||
_no_ots = ~exists(
|
||||
select(render_template_output_types.c.template_id).where(
|
||||
render_template_output_types.c.template_id == RenderTemplate.id,
|
||||
)
|
||||
)
|
||||
|
||||
# 1. Exact match: category_key + output_type in M2M
|
||||
if category_key and output_type_id:
|
||||
row = session.execute(
|
||||
select(RenderTemplate).where(and_(
|
||||
active,
|
||||
RenderTemplate.category_key == category_key,
|
||||
RenderTemplate.output_type_id == output_type_id,
|
||||
_has_ot(output_type_id),
|
||||
))
|
||||
).scalar_one_or_none()
|
||||
).unique().scalar_one_or_none()
|
||||
if row:
|
||||
return row
|
||||
|
||||
# 2. Category only
|
||||
# 2. Category only: category_key + no OTs linked
|
||||
if category_key:
|
||||
row = session.execute(
|
||||
select(RenderTemplate).where(and_(
|
||||
active,
|
||||
RenderTemplate.category_key == category_key,
|
||||
RenderTemplate.output_type_id.is_(None),
|
||||
_no_ots,
|
||||
))
|
||||
).scalar_one_or_none()
|
||||
).unique().scalar_one_or_none()
|
||||
if row:
|
||||
return row
|
||||
|
||||
# 3. OT only
|
||||
# 3. OT only: no category_key + output_type in M2M
|
||||
if output_type_id:
|
||||
row = session.execute(
|
||||
select(RenderTemplate).where(and_(
|
||||
active,
|
||||
RenderTemplate.category_key.is_(None),
|
||||
RenderTemplate.output_type_id == output_type_id,
|
||||
_has_ot(output_type_id),
|
||||
))
|
||||
).scalar_one_or_none()
|
||||
).unique().scalar_one_or_none()
|
||||
if row:
|
||||
return row
|
||||
|
||||
# 4. Global fallback (both NULL)
|
||||
# 4. Global fallback: no category_key + no OTs linked
|
||||
row = session.execute(
|
||||
select(RenderTemplate).where(and_(
|
||||
active,
|
||||
RenderTemplate.category_key.is_(None),
|
||||
RenderTemplate.output_type_id.is_(None),
|
||||
_no_ots,
|
||||
))
|
||||
).scalar_one_or_none()
|
||||
return row
|
||||
|
||||
|
||||
def get_material_library_path() -> str | None:
|
||||
"""Read material_library_path from system_settings. Returns None if empty."""
|
||||
"""Return the blend_file_path of the first active AssetLibrary.
|
||||
|
||||
Falls back to the legacy material_library_path system setting.
|
||||
"""
|
||||
engine = _get_engine()
|
||||
with Session(engine) as session:
|
||||
# Prefer active AssetLibrary
|
||||
from app.domains.materials.models import AssetLibrary
|
||||
row = session.execute(
|
||||
select(AssetLibrary).where(AssetLibrary.is_active == True).limit(1) # noqa: E712
|
||||
).scalar_one_or_none()
|
||||
if row and row.blend_file_path:
|
||||
return row.blend_file_path
|
||||
|
||||
# Fallback to legacy system setting
|
||||
row = session.execute(
|
||||
select(SystemSetting).where(SystemSetting.key == "material_library_path")
|
||||
).scalar_one_or_none()
|
||||
|
||||
Reference in New Issue
Block a user