using System;
namespace IvfTl.AutoFocus.Layout
{
///
/// 拍摄层位置计算 + 配置就近优先解析(M2-02,纯逻辑)。
///
/// 依据:
/// - 需求文档/12-工作计划表与自动对焦数据设计.md §2.4(层位置公式)、§2.5(配置就近优先)。
/// - sql/migrations/2026-06-17-autofocus-data-layer.sql
/// tl_setting.focus_layer_spacing_pulse / focus_layer_count / focus_layer_down
/// house_well_setting.focus_layer_spacing_pulse / focus_layer_count(well 级覆盖)
/// 下移层数复用 house_well_setting.move_down_layer。
///
/// 设计约束(M2-02 子计划):
/// - 纯函数类,仅引用 BCL(System),不引用 HAL/SqlSugar/WPF,可移植到独立测试项目。
/// - FocusZ(动态锚点,对焦算出)与拍摄层参数(静态配置)正交结合。
/// - §2.5 不用魔法数兜底:层间距两级皆空 → 抛 FocusConfigMissingException。
/// - EEPROM 值不进解析链(仅别处界面参考显示)。
///
/// 取数与解析解耦:调用方(M2-03/M2-04/M2-07)负责从 DB 读
/// tl_setting / house_well_setting 填进 FocusLayerRawConfig,本类只做就近优先解析与公式计算。
///
public static class PhotoLayerCalculator
{
///
/// §2.5 就近优先解析:well 级覆盖(非空) > 设备级 > 报错。
/// 层间距(spacingPulse)两级皆空 → 抛 (不兜底)。
/// 下移层数:well.MoveDownLayer 非空则覆盖 device.LayerDown(复用 move_down_layer)。
/// 层数:well.LayerCount 非空则覆盖 device.LayerCount。
/// 层数/下移设备级有 SQL DEFAULT(5/2),正常不会抛。
/// EEPROM 值不在入参中,故天然不进解析链。
///
/// raw 为 null。
/// 层间距 well 级与设备级皆为空。
public static FocusLayerConfig Resolve(FocusLayerRawConfig raw)
{
if (raw == null) throw new ArgumentNullException(nameof(raw));
// 层间距:就近优先 well → 设备级 → 报错(不用魔法数)。
int? spacing = raw.WellSpacingPulse ?? raw.DeviceSpacingPulse;
if (!spacing.HasValue)
{
throw new FocusConfigMissingException(raw.TlSn, raw.HouseSn, raw.WellSn);
}
// 层数:well 覆盖 → 设备级(设备级有 DEFAULT 5)。设备级也缺则同样视为未配置报错。
int? count = raw.WellLayerCount ?? raw.DeviceLayerCount;
if (!count.HasValue)
{
throw new FocusConfigMissingException(
raw.TlSn, raw.HouseSn, raw.WellSn,
$"设备{raw.TlSn}对焦层数未配置,请先初始化");
}
// 下移:well.MoveDownLayer 非空覆盖 device.LayerDown(复用 move_down_layer,设备级 DEFAULT 2)。
int? down = raw.WellMoveDownLayer ?? raw.DeviceLayerDown;
if (!down.HasValue)
{
throw new FocusConfigMissingException(
raw.TlSn, raw.HouseSn, raw.WellSn,
$"设备{raw.TlSn}对焦下移层数未配置,请先初始化");
}
// 校验:下移层数须 < 层数,否则清晰层(focusZ 锚点=第 down 层)落在拍摄层数组之外。
// 与 HouseDebugPageViewModel.ResolveEffectiveTune 手调路径校验对齐,使自动对焦链路同样兜底(非仅 ViewModel 手调)。
if (down.Value >= count.Value)
{
throw new FocusConfigMissingException(
raw.TlSn, raw.HouseSn, raw.WellSn,
$"设备{raw.TlSn}对焦下移层数必须小于层数({count.Value}),否则清晰层落在拍摄范围外");
}
return new FocusLayerConfig
{
LayerSpacingPulse = spacing.Value,
LayerCount = count.Value,
LayerDown = down.Value,
};
}
///
/// §2.4 层位置公式:
/// 对焦起点(第0层) = focusZ − LayerDown × LayerSpacingPulse;
/// 第 i 层 = 对焦起点 + i × LayerSpacingPulse (i = 0 .. LayerCount-1)。
/// 返回各层绝对 Z 脉冲(长度 = LayerCount)。
/// pulseMax > 0 时对越界层钳位到 [0, pulseMax](对齐 verticalMotorPulseMax)。
///
/// 对焦算出的最清晰层 Z 脉冲(锚点)。
/// 已解析好的有效配置。
/// 垂直电机脉冲上限;<=0 表示不钳位。
/// cfg 为 null。
/// LayerCount <= 0。
public static int[] ComputeLayerPositions(int focusZ, FocusLayerConfig cfg, int pulseMax = 0)
{
if (cfg == null) throw new ArgumentNullException(nameof(cfg));
if (cfg.LayerCount <= 0)
{
throw new ArgumentOutOfRangeException(
nameof(cfg), cfg.LayerCount, "LayerCount 必须 > 0");
}
int spacing = cfg.LayerSpacingPulse;
// 对焦起点(第0层) = focusZ − down × spacing。
int start = focusZ - cfg.LayerDown * spacing;
var positions = new int[cfg.LayerCount];
for (int i = 0; i < cfg.LayerCount; i++)
{
int z = start + i * spacing;
if (pulseMax > 0)
{
// 越界钳位到 [0, pulseMax]。
if (z < 0) z = 0;
else if (z > pulseMax) z = pulseMax;
}
positions[i] = z;
}
return positions;
}
}
}