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
+14 -6
View File
@@ -10,7 +10,9 @@ SENSOR_WIDTH_MM = 36.0
FILL_FACTOR = 0.85
def setup_auto_camera(parts: list, width: int, height: int):
def setup_auto_camera(parts: list, width: int, height: int,
lens_mm: float | None = None,
sensor_width_mm: float | None = None):
"""Compute bounding sphere and place an isometric auto-camera.
Returns (bbox_center, bsphere_radius) as a tuple so the caller can
@@ -19,6 +21,9 @@ def setup_auto_camera(parts: list, width: int, height: int):
import bpy # type: ignore[import]
from mathutils import Vector, Matrix # type: ignore[import]
_lens = lens_mm if lens_mm is not None else LENS_MM
_sensor = sensor_width_mm if sensor_width_mm is not None else SENSOR_WIDTH_MM
all_corners = []
for part in parts:
all_corners.extend(part.matrix_world @ Vector(c) for c in part.bound_box)
@@ -50,18 +55,21 @@ def setup_auto_camera(parts: list, width: int, height: int):
math.sin(elevation_rad),
)).normalized()
fov_h = math.atan(SENSOR_WIDTH_MM / (2.0 * LENS_MM))
fov_v = math.atan(SENSOR_WIDTH_MM * (height / width) / (2.0 * LENS_MM))
fov_h = math.atan(_sensor / (2.0 * _lens))
fov_v = math.atan(_sensor * (height / width) / (2.0 * _lens))
fov_used = min(fov_h, fov_v)
dist = (bsphere_radius / math.tan(fov_used)) / FILL_FACTOR
dist = max(dist, bsphere_radius * 1.5)
print(f"[blender_render] camera dist={dist:.4f}, fov={math.degrees(fov_used):.2f}°")
# Minimum distance: prevent camera from being inside the bounding sphere,
# but scale with FOV so wide-angle lenses can still frame correctly.
min_dist = bsphere_radius * 1.05 # just outside the sphere surface
dist = max(dist, min_dist)
print(f"[blender_render] camera dist={dist:.4f}, fov={math.degrees(fov_used):.2f}°, lens={_lens}mm")
cam_location = bbox_center + cam_dir * dist
bpy.ops.object.camera_add(location=cam_location)
cam_obj = bpy.context.active_object
cam_obj.data.lens = LENS_MM
cam_obj.data.lens = _lens
bpy.context.scene.camera = cam_obj
look_dir = (bbox_center - cam_location).normalized()