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