feat: per-position camera settings, material alias dialog, product delete, media browser links

- Per-render-position focal_length_mm/sensor_width_mm (DB → pipeline → Blender)
- FOV-based camera distance with min clamp fix for wide-angle lenses
- Unmapped materials blocking dialog on "Dispatch Renders" with batch alias creation
- Material check endpoint (GET /orders/{id}/check-materials)
- Batch alias endpoint (POST /materials/batch-aliases)
- Quick-map "No alias" badges on Materials page
- Full product hard-delete with storage cleanup (MinIO + disk files + orphaned CadFile)
- Delete button on ProductDetail page with confirmation
- Clickable product names in Media Browser (links to product page)
- Single-line render dispatch/retry (POST /orders/{id}/lines/{id}/dispatch-render)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 12:16:37 +01:00
parent 0020376702
commit b583b0d7a2
48 changed files with 1827 additions and 376 deletions
+48 -7
View File
@@ -8,12 +8,45 @@ import time as _time
FAILED_MATERIAL_NAME = "SCHAEFFLER_059999_FailedMaterial"
def _find_material_with_nodes(base_name: str):
"""Find a material by name that actually has shader nodes.
Blender's USD importer creates empty stub materials (use_nodes=True,
node_tree has 0 nodes) from USD material bindings. When we later
append the real material from a .blend library, Blender renames it
with a .001/.002 suffix to avoid the name collision.
This helper searches bpy.data.materials for the version that has
actual shader nodes, preferring exact name match, then .NNN suffixes.
"""
import bpy # type: ignore[import]
# Exact name first
exact = bpy.data.materials.get(base_name)
if exact and exact.node_tree and len(exact.node_tree.nodes) > 0:
return exact
# Search for .NNN suffixed versions
for mat in bpy.data.materials:
if not mat.name.startswith(base_name):
continue
suffix = mat.name[len(base_name):]
if suffix == "" or _re.match(r'^\.\d{3}$', suffix):
if mat.node_tree and len(mat.node_tree.nodes) > 0:
return mat
return None
def _batch_append_materials(mat_lib_path: str, names: set[str]) -> dict:
"""Append multiple materials from a .blend file in a single open.
Uses bpy.data.libraries.load() to open the .blend once instead of
N separate bpy.ops.wm.append() calls (each reopens the file).
Falls back to individual append for any materials that fail to load.
Handles empty material stubs left by Blender's USD importer: when a
stub exists with the target name, the library material gets renamed
with a .NNN suffix. We find it via _find_material_with_nodes().
"""
import bpy # type: ignore[import]
@@ -28,12 +61,16 @@ def _batch_append_materials(mat_lib_path: str, names: set[str]) -> dict:
to_load = [n for n in names if n in available]
not_found = names - available
data_to.materials = to_load
# After the context manager closes, materials are loaded into bpy.data
# After the context manager closes, materials are loaded into bpy.data.
# If a USD stub occupied the name, the real material gets a .NNN suffix.
for mat_name in to_load:
mat = bpy.data.materials.get(mat_name)
mat = _find_material_with_nodes(mat_name)
if mat:
result[mat_name] = mat
print(f"[blender_render] batch-appended material: {mat_name}")
if mat.name != mat_name:
print(f"[blender_render] batch-appended material: {mat_name} (as '{mat.name}', stub collision)")
else:
print(f"[blender_render] batch-appended material: {mat_name}")
else:
print(f"[blender_render] WARNING: material '{mat_name}' not found after batch append")
if not_found:
@@ -51,7 +88,7 @@ def _batch_append_materials(mat_lib_path: str, names: set[str]) -> dict:
filename=mat_name,
link=False,
)
mat = bpy.data.materials.get(mat_name)
mat = _find_material_with_nodes(mat_name)
if mat:
result[mat_name] = mat
except Exception:
@@ -141,11 +178,15 @@ def apply_material_library_direct(
# Batch-append materials from library (single file open)
appended: dict = {}
_t_append = _time.monotonic()
# Check already-loaded materials first
# Check already-loaded materials first — but skip empty stubs created by
# Blender's USD importer (use_nodes=True but node_tree has 0 nodes).
# Those stubs must be loaded from the library via _batch_append_materials
# which uses _find_material_with_nodes() to resolve stub collisions.
still_needed = set()
for mat_name in needed:
if mat_name in bpy.data.materials:
appended[mat_name] = bpy.data.materials[mat_name]
existing = _find_material_with_nodes(mat_name)
if existing:
appended[mat_name] = existing
else:
still_needed.add(mat_name)
# Load remaining from .blend in one pass