feat: extract workflow bbox services phase 3
This commit is contained in:
@@ -17,59 +17,17 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _bbox_from_glb(glb_path: str) -> dict | None:
|
||||
"""Extract bounding box from a GLB file (meters → converted to mm).
|
||||
"""Backward-compatible wrapper for GLB bbox extraction."""
|
||||
from app.domains.rendering.workflow_runtime_services import extract_bbox_from_glb
|
||||
|
||||
Returns {"dimensions_mm": {x,y,z}, "bbox_center_mm": {x,y,z}} or None on failure.
|
||||
OCC GLB output is in meters; multiply by 1000 to get mm.
|
||||
"""
|
||||
try:
|
||||
import trimesh
|
||||
p = Path(glb_path)
|
||||
if not p.exists():
|
||||
return None
|
||||
scene = trimesh.load(str(p), force="scene")
|
||||
bounds = getattr(scene, "bounds", None)
|
||||
if bounds is None:
|
||||
return None
|
||||
mins, maxs = bounds
|
||||
dims = maxs - mins
|
||||
return {
|
||||
"dimensions_mm": {
|
||||
"x": round(float(dims[0]) * 1000, 2),
|
||||
"y": round(float(dims[1]) * 1000, 2),
|
||||
"z": round(float(dims[2]) * 1000, 2),
|
||||
},
|
||||
"bbox_center_mm": {
|
||||
"x": round(float((mins[0] + maxs[0]) / 2) * 1000, 2),
|
||||
"y": round(float((mins[1] + maxs[1]) / 2) * 1000, 2),
|
||||
"z": round(float((mins[2] + maxs[2]) / 2) * 1000, 2),
|
||||
},
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.debug(f"_bbox_from_glb failed for {glb_path}: {exc}")
|
||||
return None
|
||||
return extract_bbox_from_glb(glb_path)
|
||||
|
||||
|
||||
def _bbox_from_step_cadquery(step_path: str) -> dict | None:
|
||||
"""Fallback: extract bounding box by re-parsing STEP via cadquery."""
|
||||
try:
|
||||
import cadquery as cq
|
||||
bb = cq.importers.importStep(step_path).val().BoundingBox()
|
||||
return {
|
||||
"dimensions_mm": {
|
||||
"x": round(bb.xlen, 2),
|
||||
"y": round(bb.ylen, 2),
|
||||
"z": round(bb.zlen, 2),
|
||||
},
|
||||
"bbox_center_mm": {
|
||||
"x": round((bb.xmin + bb.xmax) / 2, 2),
|
||||
"y": round((bb.ymin + bb.ymax) / 2, 2),
|
||||
"z": round((bb.zmin + bb.zmax) / 2, 2),
|
||||
},
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.debug(f"_bbox_from_step_cadquery failed for {step_path}: {exc}")
|
||||
return None
|
||||
"""Backward-compatible wrapper for STEP bbox fallback extraction."""
|
||||
from app.domains.rendering.workflow_runtime_services import extract_bbox_from_step_cadquery
|
||||
|
||||
return extract_bbox_from_step_cadquery(step_path)
|
||||
|
||||
|
||||
@celery_app.task(bind=True, name="app.tasks.step_tasks.process_step_file", queue="step_processing")
|
||||
@@ -267,6 +225,7 @@ def reextract_cad_metadata(cad_file_id: str):
|
||||
from sqlalchemy.orm import Session
|
||||
from app.config import settings as app_settings
|
||||
from app.models.cad_file import CadFile
|
||||
from app.domains.rendering.workflow_runtime_services import resolve_cad_bbox
|
||||
|
||||
pl = PipelineLogger(task_id=None)
|
||||
pl.step_start("reextract_cad_metadata", {"cad_file_id": cad_file_id})
|
||||
@@ -289,7 +248,8 @@ def reextract_cad_metadata(cad_file_id: str):
|
||||
try:
|
||||
p = Path(step_path)
|
||||
glb_path = p.parent / f"{p.stem}_thumbnail.glb"
|
||||
patch = _bbox_from_glb(str(glb_path)) or _bbox_from_step_cadquery(step_path)
|
||||
bbox_result = resolve_cad_bbox(step_path, glb_path=str(glb_path))
|
||||
patch = bbox_result.bbox_data
|
||||
if patch:
|
||||
with Session(eng) as session:
|
||||
set_tenant_context_sync(session, _tenant_id)
|
||||
|
||||
@@ -117,7 +117,7 @@ def render_step_thumbnail(self, cad_file_id: str):
|
||||
# ── Post-render: bbox + sharp edges + materials (single session) ──────
|
||||
try:
|
||||
from app.models.cad_file import CadFile
|
||||
from app.domains.pipeline.tasks.extract_metadata import _bbox_from_glb, _bbox_from_step_cadquery
|
||||
from app.domains.rendering.workflow_runtime_services import resolve_cad_bbox
|
||||
|
||||
with _pipeline_session(_tenant_id) as session:
|
||||
cad = session.get(CadFile, cad_file_id)
|
||||
@@ -131,7 +131,7 @@ def render_step_thumbnail(self, cad_file_id: str):
|
||||
if step_path and not attrs.get("dimensions_mm"):
|
||||
_step = Path(step_path)
|
||||
_glb = _step.parent / f"{_step.stem}_thumbnail.glb"
|
||||
bbox_data = _bbox_from_glb(str(_glb)) or _bbox_from_step_cadquery(step_path)
|
||||
bbox_data = resolve_cad_bbox(step_path, glb_path=str(_glb)).bbox_data
|
||||
if bbox_data:
|
||||
cad.mesh_attributes = {**attrs, **bbox_data}
|
||||
attrs = cad.mesh_attributes
|
||||
|
||||
@@ -87,6 +87,18 @@ class AutoPopulateMaterialsResult:
|
||||
cad_parts: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BBoxResolutionResult:
|
||||
bbox_data: dict[str, dict[str, float]] | None
|
||||
source_kind: Literal["glb", "step", "none"]
|
||||
step_path: str
|
||||
glb_path: str | None = None
|
||||
|
||||
@property
|
||||
def has_bbox(self) -> bool:
|
||||
return self.bbox_data is not None
|
||||
|
||||
|
||||
def _emit(emit: EmitFn, order_line_id: str, message: str, level: str | None = None) -> None:
|
||||
if emit is None:
|
||||
return
|
||||
@@ -105,6 +117,86 @@ def _resolve_asset_path(storage_key: str | None) -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
def extract_bbox_from_glb(glb_path: str) -> dict[str, dict[str, float]] | None:
|
||||
"""Extract a bounding box from a GLB file in meters and convert to mm."""
|
||||
try:
|
||||
import trimesh
|
||||
|
||||
path = Path(glb_path)
|
||||
if not path.exists():
|
||||
return None
|
||||
scene = trimesh.load(str(path), force="scene")
|
||||
bounds = getattr(scene, "bounds", None)
|
||||
if bounds is None:
|
||||
return None
|
||||
mins, maxs = bounds
|
||||
dims = maxs - mins
|
||||
return {
|
||||
"dimensions_mm": {
|
||||
"x": round(float(dims[0]) * 1000, 2),
|
||||
"y": round(float(dims[1]) * 1000, 2),
|
||||
"z": round(float(dims[2]) * 1000, 2),
|
||||
},
|
||||
"bbox_center_mm": {
|
||||
"x": round(float((mins[0] + maxs[0]) / 2) * 1000, 2),
|
||||
"y": round(float((mins[1] + maxs[1]) / 2) * 1000, 2),
|
||||
"z": round(float((mins[2] + maxs[2]) / 2) * 1000, 2),
|
||||
},
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.debug("extract_bbox_from_glb failed for %s: %s", glb_path, exc)
|
||||
return None
|
||||
|
||||
|
||||
def extract_bbox_from_step_cadquery(step_path: str) -> dict[str, dict[str, float]] | None:
|
||||
"""Fallback: extract a bounding box by re-parsing the STEP file via cadquery."""
|
||||
try:
|
||||
import cadquery as cq
|
||||
|
||||
bb = cq.importers.importStep(step_path).val().BoundingBox()
|
||||
return {
|
||||
"dimensions_mm": {
|
||||
"x": round(bb.xlen, 2),
|
||||
"y": round(bb.ylen, 2),
|
||||
"z": round(bb.zlen, 2),
|
||||
},
|
||||
"bbox_center_mm": {
|
||||
"x": round((bb.xmin + bb.xmax) / 2, 2),
|
||||
"y": round((bb.ymin + bb.ymax) / 2, 2),
|
||||
"z": round((bb.zmin + bb.zmax) / 2, 2),
|
||||
},
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.debug("extract_bbox_from_step_cadquery failed for %s: %s", step_path, exc)
|
||||
return None
|
||||
|
||||
|
||||
def resolve_cad_bbox(
|
||||
step_path: str,
|
||||
*,
|
||||
glb_path: str | None = None,
|
||||
) -> BBoxResolutionResult:
|
||||
"""Resolve CAD bounding-box data with the legacy GLB-first fallback order."""
|
||||
bbox_data = None
|
||||
source_kind: Literal["glb", "step", "none"] = "none"
|
||||
if glb_path:
|
||||
bbox_data = extract_bbox_from_glb(glb_path)
|
||||
if bbox_data:
|
||||
source_kind = "glb"
|
||||
|
||||
if bbox_data is None:
|
||||
bbox_data = extract_bbox_from_step_cadquery(step_path)
|
||||
if bbox_data:
|
||||
source_kind = "step"
|
||||
|
||||
return BBoxResolutionResult(
|
||||
bbox_data=bbox_data,
|
||||
source_kind=source_kind,
|
||||
step_path=step_path,
|
||||
glb_path=glb_path,
|
||||
)
|
||||
|
||||
|
||||
def prepare_order_line_render_context(
|
||||
session: Session,
|
||||
order_line_id: str,
|
||||
|
||||
Reference in New Issue
Block a user