fix: smooth normals on non-indexed geometry + sync DB in gltf task

InlineCadViewer: STL-derived GLBs have non-indexed geometry (unique vertex
per triangle face). computeVertexNormals() on non-indexed geometry produces
per-face normals (faceted shading). Fix: mergeVertices() first to create
shared/indexed geometry, then computeVertexNormals() averages across
adjacent faces → smooth shading. Indexed Blender GLBs are unaffected.

generate_gltf_geometry_task: asyncio.run() inside a Celery worker that
already runs asyncpg causes 'Future attached to a different loop'. Replace
async _store() with sync SQLAlchemy session (matching the rest of the task).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 15:17:20 +01:00
parent e2eda92d82
commit 2377cb192a
2 changed files with 136 additions and 49 deletions
+28 -29
View File
@@ -530,38 +530,37 @@ def generate_gltf_geometry_task(self, cad_file_id: str):
log_task_event(self.request.id, f"Blender GLB export completed: {output_path.name}", "done")
# --- Store MediaAsset (replace existing gltf_geometry for this cad_file) ---
import asyncio
# Use sync SQLAlchemy to avoid asyncio event-loop conflicts in Celery workers.
import uuid as _uuid
from sqlalchemy import create_engine as _ce, delete as _del
from sqlalchemy.orm import Session as _Session
from app.domains.media.models import MediaAsset, MediaAssetType
async def _store():
from app.database import AsyncSessionLocal
from app.domains.media.models import MediaAsset, MediaAssetType
from app.config import settings as _cfg
import uuid
async with AsyncSessionLocal() as db:
# Delete previous gltf_geometry assets for this cad_file to avoid stale records
from sqlalchemy import delete as _delete
await db.execute(
_delete(MediaAsset).where(
MediaAsset.cad_file_id == uuid.UUID(cad_file_id),
MediaAsset.asset_type == MediaAssetType.gltf_geometry,
)
_sync_url = app_settings.database_url.replace("+asyncpg", "")
_eng2 = _ce(_sync_url)
with _Session(_eng2) as _sess:
_sess.execute(
_del(MediaAsset).where(
MediaAsset.cad_file_id == _uuid.UUID(cad_file_id),
MediaAsset.asset_type == MediaAssetType.gltf_geometry,
)
_key = str(output_path)
_prefix = str(_cfg.upload_dir).rstrip("/") + "/"
if _key.startswith(_prefix):
_key = _key[len(_prefix):]
asset = MediaAsset(
cad_file_id=uuid.UUID(cad_file_id),
asset_type=MediaAssetType.gltf_geometry,
storage_key=_key,
mime_type="model/gltf-binary",
file_size_bytes=output_path.stat().st_size if output_path.exists() else None,
)
db.add(asset)
await db.commit()
return str(asset.id)
)
_key = str(output_path)
_prefix = str(app_settings.upload_dir).rstrip("/") + "/"
if _key.startswith(_prefix):
_key = _key[len(_prefix):]
asset = MediaAsset(
cad_file_id=_uuid.UUID(cad_file_id),
asset_type=MediaAssetType.gltf_geometry,
storage_key=_key,
mime_type="model/gltf-binary",
file_size_bytes=output_path.stat().st_size if output_path.exists() else None,
)
_sess.add(asset)
_sess.commit()
asset_id = str(asset.id)
_eng2.dispose()
asset_id = asyncio.run(_store())
logger.info("generate_gltf_geometry_task: MediaAsset %s created for cad %s", asset_id, cad_file_id)
return {"glb_path": str(output_path), "asset_id": asset_id}