fix: align workflow material resolution with scene manifest
This commit is contained in:
@@ -17,13 +17,14 @@ from app.core.render_paths import resolve_result_path, result_path_to_storage_ke
|
||||
from app.domains.media.models import MediaAsset, MediaAssetType
|
||||
from app.domains.orders.models import Order, OrderLine, OrderStatus
|
||||
from app.domains.products.models import CadFile, Product
|
||||
from app.domains.rendering.output_type_contracts import merge_output_type_invocation_overrides
|
||||
from app.domains.rendering.output_type_contracts import resolve_output_type_invocation_overrides
|
||||
from app.domains.rendering.models import (
|
||||
GlobalRenderPosition,
|
||||
ProductRenderPosition,
|
||||
RenderTemplate,
|
||||
WorkflowRun,
|
||||
)
|
||||
from app.services.part_key_service import build_scene_manifest
|
||||
from app.services.material_service import resolve_material_map
|
||||
from app.services.step_processor import build_part_colors
|
||||
from app.services.template_service import (
|
||||
@@ -459,7 +460,11 @@ def _normalize_storage_key(output_path: str) -> str:
|
||||
|
||||
def _resolve_output_asset_type(output_path: str) -> MediaAssetType:
|
||||
extension = output_path.rsplit(".", 1)[-1].lower() if "." in output_path else "bin"
|
||||
return MediaAssetType.turntable if extension in ("mp4", "webm") else MediaAssetType.still
|
||||
if extension == "blend":
|
||||
return MediaAssetType.blend_production
|
||||
if extension in ("mp4", "webm"):
|
||||
return MediaAssetType.turntable
|
||||
return MediaAssetType.still
|
||||
|
||||
|
||||
def _resolve_output_mime_type(output_path: str) -> str:
|
||||
@@ -541,6 +546,7 @@ def build_order_line_render_invocation(
|
||||
template_context: TemplateResolutionResult | None = None,
|
||||
position_context: RenderPositionContext | None = None,
|
||||
material_context: MaterialResolutionResult | None = None,
|
||||
artifact_kind_override: str | None = None,
|
||||
emit: EmitFn = None,
|
||||
) -> OrderLineRenderInvocation:
|
||||
if not setup.is_ready or setup.order_line is None or setup.cad_file is None:
|
||||
@@ -551,9 +557,11 @@ def build_order_line_render_invocation(
|
||||
output_type = line.output_type
|
||||
position = position_context or RenderPositionContext()
|
||||
render_settings = (
|
||||
merge_output_type_invocation_overrides(
|
||||
resolve_output_type_invocation_overrides(
|
||||
output_type.render_settings,
|
||||
getattr(output_type, "invocation_overrides", None),
|
||||
artifact_kind=artifact_kind_override or output_type.artifact_kind,
|
||||
is_animation=output_type.is_animation,
|
||||
)
|
||||
if output_type is not None
|
||||
else {}
|
||||
@@ -1191,6 +1199,10 @@ def resolve_order_line_template_context(
|
||||
setup: OrderLineRenderSetupResult,
|
||||
*,
|
||||
emit: EmitFn = None,
|
||||
template_id_override: str | None = None,
|
||||
material_library_path_override: str | None = None,
|
||||
require_template: bool = False,
|
||||
disable_materials: bool = False,
|
||||
) -> TemplateResolutionResult:
|
||||
"""Resolve render template, material library, and material map for a prepared order line."""
|
||||
if not setup.is_ready:
|
||||
@@ -1204,12 +1216,32 @@ def resolve_order_line_template_context(
|
||||
category_key = line.product.category_key if line.product else None
|
||||
output_type_id = str(line.output_type_id) if line.output_type_id else None
|
||||
|
||||
template = resolve_template_for_session(
|
||||
session,
|
||||
category_key=category_key,
|
||||
output_type_id=output_type_id,
|
||||
template = None
|
||||
if template_id_override:
|
||||
try:
|
||||
template_uuid = uuid.UUID(str(template_id_override))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(f"template_id_override is not a valid UUID: {template_id_override}") from exc
|
||||
template = session.get(RenderTemplate, template_uuid)
|
||||
if template is None:
|
||||
raise ValueError(f"render template not found: {template_id_override}")
|
||||
if not template.is_active:
|
||||
raise ValueError(f"render template is inactive: {template_id_override}")
|
||||
else:
|
||||
template = resolve_template_for_session(
|
||||
session,
|
||||
category_key=category_key,
|
||||
output_type_id=output_type_id,
|
||||
)
|
||||
|
||||
if require_template and template is None:
|
||||
raise ValueError("resolve_order_line_template_context requires a matching render template")
|
||||
|
||||
material_library = (
|
||||
material_library_path_override.strip()
|
||||
if isinstance(material_library_path_override, str) and material_library_path_override.strip()
|
||||
else get_material_library_path_for_session(session)
|
||||
)
|
||||
material_library = get_material_library_path_for_session(session)
|
||||
material_resolution = resolve_order_line_material_map(
|
||||
line,
|
||||
cad_file,
|
||||
@@ -1217,6 +1249,7 @@ def resolve_order_line_template_context(
|
||||
material_library=material_library,
|
||||
template=template,
|
||||
emit=emit,
|
||||
disable_materials=disable_materials,
|
||||
)
|
||||
|
||||
if template:
|
||||
@@ -1261,27 +1294,36 @@ def resolve_order_line_material_map(
|
||||
material_library: str | None,
|
||||
template: RenderTemplate | None,
|
||||
emit: EmitFn = None,
|
||||
material_override: str | None = None,
|
||||
disable_materials: bool = False,
|
||||
) -> MaterialResolutionResult:
|
||||
"""Resolve the effective order-line material map with legacy precedence rules."""
|
||||
if disable_materials:
|
||||
return MaterialResolutionResult(
|
||||
material_map=None,
|
||||
use_materials=False,
|
||||
override_material=material_override,
|
||||
source_material_count=0,
|
||||
resolved_material_count=0,
|
||||
)
|
||||
|
||||
material_map = None
|
||||
raw_material_count = 0
|
||||
use_materials = bool(material_library and materials_source)
|
||||
raw_material_map = _build_effective_material_lookup(cad_file, materials_source)
|
||||
use_materials = bool(material_library and raw_material_map)
|
||||
if template and not template.material_replace_enabled:
|
||||
use_materials = False
|
||||
if use_materials:
|
||||
material_map = {
|
||||
material["part_name"]: material["material"]
|
||||
for material in materials_source
|
||||
if material.get("part_name") and material.get("material")
|
||||
}
|
||||
raw_material_count = len(material_map)
|
||||
material_map = resolve_material_map(material_map)
|
||||
raw_material_count = len(raw_material_map)
|
||||
material_map = resolve_material_map(raw_material_map)
|
||||
|
||||
line_override = getattr(line, "material_override", None)
|
||||
output_override = line.output_type.material_override if line.output_type else None
|
||||
override_material = line_override or output_override
|
||||
override_material = material_override or line_override or output_override
|
||||
if override_material:
|
||||
override_keys = set(material_map.keys()) if material_map else set()
|
||||
if cad_file:
|
||||
override_keys.update(_collect_cad_material_keys(cad_file))
|
||||
if cad_file and cad_file.parsed_objects:
|
||||
for part_name in cad_file.parsed_objects.get("objects", []):
|
||||
override_keys.add(part_name)
|
||||
@@ -1302,12 +1344,71 @@ def resolve_order_line_material_map(
|
||||
)
|
||||
|
||||
|
||||
def _build_effective_material_lookup(
|
||||
cad_file: CadFile | None,
|
||||
materials_source: list[dict[str, Any]],
|
||||
) -> dict[str, str]:
|
||||
"""Build a renderer-compatible material lookup from all available layers.
|
||||
|
||||
Authoritative scene-manifest assignments win when present, but we emit both
|
||||
source-name and part-key keys so USD and GLB/STEP fallback paths resolve the
|
||||
same effective material map.
|
||||
"""
|
||||
raw_material_map: dict[str, str] = {
|
||||
str(material["part_name"]): str(material["material"])
|
||||
for material in materials_source
|
||||
if material.get("part_name") and material.get("material")
|
||||
}
|
||||
|
||||
if not cad_file:
|
||||
return raw_material_map
|
||||
|
||||
manifest = build_scene_manifest(cad_file)
|
||||
for part in manifest.get("parts", []):
|
||||
if not isinstance(part, dict):
|
||||
continue
|
||||
effective_material = part.get("effective_material")
|
||||
if not effective_material:
|
||||
continue
|
||||
source_name = part.get("source_name")
|
||||
part_key = part.get("part_key")
|
||||
if source_name:
|
||||
raw_material_map[str(source_name)] = str(effective_material)
|
||||
if part_key:
|
||||
raw_material_map[str(part_key)] = str(effective_material)
|
||||
|
||||
return raw_material_map
|
||||
|
||||
|
||||
def _collect_cad_material_keys(cad_file: CadFile) -> set[str]:
|
||||
if not (
|
||||
cad_file.resolved_material_assignments
|
||||
or cad_file.manual_material_overrides
|
||||
or cad_file.source_material_assignments
|
||||
):
|
||||
return set()
|
||||
|
||||
keys: set[str] = set()
|
||||
manifest = build_scene_manifest(cad_file)
|
||||
for part in manifest.get("parts", []):
|
||||
if not isinstance(part, dict):
|
||||
continue
|
||||
source_name = part.get("source_name")
|
||||
part_key = part.get("part_key")
|
||||
if source_name:
|
||||
keys.add(str(source_name))
|
||||
if part_key:
|
||||
keys.add(str(part_key))
|
||||
return keys
|
||||
|
||||
|
||||
def auto_populate_materials_for_cad(
|
||||
session: Session,
|
||||
cad_file_id: str,
|
||||
*,
|
||||
enqueue_thumbnail: QueueThumbnailFn = None,
|
||||
persist_updates: bool = True,
|
||||
include_populated_products: bool = False,
|
||||
) -> AutoPopulateMaterialsResult:
|
||||
"""Auto-fill empty CAD material mappings from Excel component data.
|
||||
|
||||
@@ -1347,7 +1448,7 @@ def auto_populate_materials_for_cad(
|
||||
continue
|
||||
|
||||
existing = product.cad_part_materials or []
|
||||
if existing and any((entry.get("material") or "").strip() for entry in existing):
|
||||
if not include_populated_products and existing and any((entry.get("material") or "").strip() for entry in existing):
|
||||
continue
|
||||
|
||||
new_materials = build_materials_from_excel(cad_parts, excel_components)
|
||||
|
||||
Reference in New Issue
Block a user