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:
@@ -41,6 +41,11 @@ SETTINGS_DEFAULTS: dict[str, str] = {
|
||||
"smtp_user": "",
|
||||
"smtp_password": "",
|
||||
"smtp_from_address": "",
|
||||
# glTF tessellation quality (OCC BRepMesh)
|
||||
"gltf_preview_linear_deflection": "0.1", # mm — geometry GLB for viewer
|
||||
"gltf_preview_angular_deflection": "0.5", # rad
|
||||
"gltf_production_linear_deflection": "0.03", # mm — production GLB
|
||||
"gltf_production_angular_deflection": "0.2", # rad
|
||||
# 3D viewer / glTF export settings
|
||||
"gltf_scale_factor": "0.001",
|
||||
"gltf_smooth_normals": "true",
|
||||
@@ -71,6 +76,10 @@ class SettingsOut(BaseModel):
|
||||
smtp_user: str = ""
|
||||
smtp_password: str = ""
|
||||
smtp_from_address: str = ""
|
||||
gltf_preview_linear_deflection: float = 0.1
|
||||
gltf_preview_angular_deflection: float = 0.5
|
||||
gltf_production_linear_deflection: float = 0.03
|
||||
gltf_production_angular_deflection: float = 0.2
|
||||
gltf_scale_factor: float = 0.001
|
||||
gltf_smooth_normals: bool = True
|
||||
viewer_max_distance: float = 50.0
|
||||
@@ -99,6 +108,10 @@ class SettingsUpdate(BaseModel):
|
||||
smtp_user: str | None = None
|
||||
smtp_password: str | None = None
|
||||
smtp_from_address: str | None = None
|
||||
gltf_preview_linear_deflection: float | None = None
|
||||
gltf_preview_angular_deflection: float | None = None
|
||||
gltf_production_linear_deflection: float | None = None
|
||||
gltf_production_angular_deflection: float | None = None
|
||||
gltf_scale_factor: float | None = None
|
||||
gltf_smooth_normals: bool | None = None
|
||||
viewer_max_distance: float | None = None
|
||||
@@ -213,6 +226,10 @@ def _settings_to_out(raw: dict[str, str]) -> SettingsOut:
|
||||
smtp_user=raw.get("smtp_user", ""),
|
||||
smtp_password=raw.get("smtp_password", ""),
|
||||
smtp_from_address=raw.get("smtp_from_address", ""),
|
||||
gltf_preview_linear_deflection=float(raw.get("gltf_preview_linear_deflection", "0.1")),
|
||||
gltf_preview_angular_deflection=float(raw.get("gltf_preview_angular_deflection", "0.5")),
|
||||
gltf_production_linear_deflection=float(raw.get("gltf_production_linear_deflection", "0.03")),
|
||||
gltf_production_angular_deflection=float(raw.get("gltf_production_angular_deflection", "0.2")),
|
||||
gltf_scale_factor=float(raw.get("gltf_scale_factor", "0.001")),
|
||||
gltf_smooth_normals=raw.get("gltf_smooth_normals", "true") == "true",
|
||||
viewer_max_distance=float(raw.get("viewer_max_distance", "50")),
|
||||
@@ -328,6 +345,22 @@ async def update_settings(
|
||||
updates["gltf_pbr_roughness"] = str(body.gltf_pbr_roughness)
|
||||
if body.gltf_pbr_metallic is not None:
|
||||
updates["gltf_pbr_metallic"] = str(body.gltf_pbr_metallic)
|
||||
if body.gltf_preview_linear_deflection is not None:
|
||||
if not (0.001 <= body.gltf_preview_linear_deflection <= 10.0):
|
||||
raise HTTPException(400, detail="gltf_preview_linear_deflection must be 0.001–10.0 mm")
|
||||
updates["gltf_preview_linear_deflection"] = str(body.gltf_preview_linear_deflection)
|
||||
if body.gltf_preview_angular_deflection is not None:
|
||||
if not (0.05 <= body.gltf_preview_angular_deflection <= 1.5):
|
||||
raise HTTPException(400, detail="gltf_preview_angular_deflection must be 0.05–1.5 rad")
|
||||
updates["gltf_preview_angular_deflection"] = str(body.gltf_preview_angular_deflection)
|
||||
if body.gltf_production_linear_deflection is not None:
|
||||
if not (0.001 <= body.gltf_production_linear_deflection <= 10.0):
|
||||
raise HTTPException(400, detail="gltf_production_linear_deflection must be 0.001–10.0 mm")
|
||||
updates["gltf_production_linear_deflection"] = str(body.gltf_production_linear_deflection)
|
||||
if body.gltf_production_angular_deflection is not None:
|
||||
if not (0.05 <= body.gltf_production_angular_deflection <= 1.5):
|
||||
raise HTTPException(400, detail="gltf_production_angular_deflection must be 0.05–1.5 rad")
|
||||
updates["gltf_production_angular_deflection"] = str(body.gltf_production_angular_deflection)
|
||||
|
||||
for k, v in updates.items():
|
||||
await _save_setting(db, k, v)
|
||||
@@ -470,6 +503,40 @@ async def generate_missing_geometry_glbs(
|
||||
return {"queued": queued, "message": f"Queued {queued} missing geometry GLB task(s)"}
|
||||
|
||||
|
||||
@router.post("/settings/recover-stuck-processing", status_code=status.HTTP_200_OK)
|
||||
async def recover_stuck_processing(
|
||||
admin: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Reset CAD files stuck in 'processing' for more than 10 minutes to 'failed'.
|
||||
|
||||
Call this when a CAD file shows 'processing' indefinitely. The auto-recovery
|
||||
beat task also runs every 5 minutes, so this is just for immediate relief.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import update as sql_update, and_
|
||||
|
||||
cutoff = datetime.utcnow() - timedelta(minutes=10)
|
||||
result = await db.execute(
|
||||
sql_update(CadFile)
|
||||
.where(
|
||||
and_(
|
||||
CadFile.processing_status == ProcessingStatus.processing,
|
||||
CadFile.updated_at < cutoff,
|
||||
)
|
||||
)
|
||||
.values(
|
||||
processing_status=ProcessingStatus.failed,
|
||||
error_message="Processing timed out — worker may have crashed. Use 'Regenerate Thumbnail' to retry.",
|
||||
)
|
||||
.returning(CadFile.id)
|
||||
)
|
||||
reset_ids = [str(r[0]) for r in result.fetchall()]
|
||||
await db.commit()
|
||||
return {"reset": len(reset_ids), "ids": reset_ids,
|
||||
"message": f"Reset {len(reset_ids)} stuck file(s) to 'failed'"}
|
||||
|
||||
|
||||
@router.post("/settings/seed-workflows", status_code=status.HTTP_200_OK)
|
||||
async def seed_workflows(
|
||||
admin: User = Depends(require_admin),
|
||||
|
||||
Reference in New Issue
Block a user