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; } } }