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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user