| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- using System;
- namespace IvfTl.AutoFocus.Layout
- {
- /// <summary>
- /// 拍摄层位置计算 + 配置就近优先解析(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,本类只做就近优先解析与公式计算。
- /// </summary>
- public static class PhotoLayerCalculator
- {
- /// <summary>
- /// §2.5 就近优先解析:well 级覆盖(非空) > 设备级 > 报错。
- /// 层间距(spacingPulse)两级皆空 → 抛 <see cref="FocusConfigMissingException"/>(不兜底)。
- /// 下移层数:well.MoveDownLayer 非空则覆盖 device.LayerDown(复用 move_down_layer)。
- /// 层数:well.LayerCount 非空则覆盖 device.LayerCount。
- /// 层数/下移设备级有 SQL DEFAULT(5/2),正常不会抛。
- /// EEPROM 值不在入参中,故天然不进解析链。
- /// </summary>
- /// <exception cref="ArgumentNullException">raw 为 null。</exception>
- /// <exception cref="FocusConfigMissingException">层间距 well 级与设备级皆为空。</exception>
- 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,
- };
- }
- /// <summary>
- /// §2.4 层位置公式:
- /// 对焦起点(第0层) = focusZ − LayerDown × LayerSpacingPulse;
- /// 第 i 层 = 对焦起点 + i × LayerSpacingPulse (i = 0 .. LayerCount-1)。
- /// 返回各层绝对 Z 脉冲(长度 = LayerCount)。
- /// pulseMax > 0 时对越界层钳位到 [0, pulseMax](对齐 verticalMotorPulseMax)。
- /// </summary>
- /// <param name="focusZ">对焦算出的最清晰层 Z 脉冲(锚点)。</param>
- /// <param name="cfg">已解析好的有效配置。</param>
- /// <param name="pulseMax">垂直电机脉冲上限;<=0 表示不钳位。</param>
- /// <exception cref="ArgumentNullException">cfg 为 null。</exception>
- /// <exception cref="ArgumentOutOfRangeException">LayerCount <= 0。</exception>
- 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;
- }
- }
- }
|