PhotoLayerCalculator.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. using System;
  2. namespace IvfTl.AutoFocus.Layout
  3. {
  4. /// <summary>
  5. /// 拍摄层位置计算 + 配置就近优先解析(M2-02,纯逻辑)。
  6. ///
  7. /// 依据:
  8. /// - 需求文档/12-工作计划表与自动对焦数据设计.md §2.4(层位置公式)、§2.5(配置就近优先)。
  9. /// - sql/migrations/2026-06-17-autofocus-data-layer.sql
  10. /// tl_setting.focus_layer_spacing_pulse / focus_layer_count / focus_layer_down
  11. /// house_well_setting.focus_layer_spacing_pulse / focus_layer_count(well 级覆盖)
  12. /// 下移层数复用 house_well_setting.move_down_layer。
  13. ///
  14. /// 设计约束(M2-02 子计划):
  15. /// - 纯函数类,仅引用 BCL(System),不引用 HAL/SqlSugar/WPF,可移植到独立测试项目。
  16. /// - FocusZ(动态锚点,对焦算出)与拍摄层参数(静态配置)正交结合。
  17. /// - §2.5 不用魔法数兜底:层间距两级皆空 → 抛 FocusConfigMissingException。
  18. /// - EEPROM 值不进解析链(仅别处界面参考显示)。
  19. ///
  20. /// 取数与解析解耦:调用方(M2-03/M2-04/M2-07)负责从 DB 读
  21. /// tl_setting / house_well_setting 填进 FocusLayerRawConfig,本类只做就近优先解析与公式计算。
  22. /// </summary>
  23. public static class PhotoLayerCalculator
  24. {
  25. /// <summary>
  26. /// §2.5 就近优先解析:well 级覆盖(非空) &gt; 设备级 &gt; 报错。
  27. /// 层间距(spacingPulse)两级皆空 → 抛 <see cref="FocusConfigMissingException"/>(不兜底)。
  28. /// 下移层数:well.MoveDownLayer 非空则覆盖 device.LayerDown(复用 move_down_layer)。
  29. /// 层数:well.LayerCount 非空则覆盖 device.LayerCount。
  30. /// 层数/下移设备级有 SQL DEFAULT(5/2),正常不会抛。
  31. /// EEPROM 值不在入参中,故天然不进解析链。
  32. /// </summary>
  33. /// <exception cref="ArgumentNullException">raw 为 null。</exception>
  34. /// <exception cref="FocusConfigMissingException">层间距 well 级与设备级皆为空。</exception>
  35. public static FocusLayerConfig Resolve(FocusLayerRawConfig raw)
  36. {
  37. if (raw == null) throw new ArgumentNullException(nameof(raw));
  38. // 层间距:就近优先 well → 设备级 → 报错(不用魔法数)。
  39. int? spacing = raw.WellSpacingPulse ?? raw.DeviceSpacingPulse;
  40. if (!spacing.HasValue)
  41. {
  42. throw new FocusConfigMissingException(raw.TlSn, raw.HouseSn, raw.WellSn);
  43. }
  44. // 层数:well 覆盖 → 设备级(设备级有 DEFAULT 5)。设备级也缺则同样视为未配置报错。
  45. int? count = raw.WellLayerCount ?? raw.DeviceLayerCount;
  46. if (!count.HasValue)
  47. {
  48. throw new FocusConfigMissingException(
  49. raw.TlSn, raw.HouseSn, raw.WellSn,
  50. $"设备{raw.TlSn}对焦层数未配置,请先初始化");
  51. }
  52. // 下移:well.MoveDownLayer 非空覆盖 device.LayerDown(复用 move_down_layer,设备级 DEFAULT 2)。
  53. int? down = raw.WellMoveDownLayer ?? raw.DeviceLayerDown;
  54. if (!down.HasValue)
  55. {
  56. throw new FocusConfigMissingException(
  57. raw.TlSn, raw.HouseSn, raw.WellSn,
  58. $"设备{raw.TlSn}对焦下移层数未配置,请先初始化");
  59. }
  60. return new FocusLayerConfig
  61. {
  62. LayerSpacingPulse = spacing.Value,
  63. LayerCount = count.Value,
  64. LayerDown = down.Value,
  65. };
  66. }
  67. /// <summary>
  68. /// §2.4 层位置公式:
  69. /// 对焦起点(第0层) = focusZ − LayerDown × LayerSpacingPulse;
  70. /// 第 i 层 = 对焦起点 + i × LayerSpacingPulse (i = 0 .. LayerCount-1)。
  71. /// 返回各层绝对 Z 脉冲(长度 = LayerCount)。
  72. /// pulseMax &gt; 0 时对越界层钳位到 [0, pulseMax](对齐 verticalMotorPulseMax)。
  73. /// </summary>
  74. /// <param name="focusZ">对焦算出的最清晰层 Z 脉冲(锚点)。</param>
  75. /// <param name="cfg">已解析好的有效配置。</param>
  76. /// <param name="pulseMax">垂直电机脉冲上限;&lt;=0 表示不钳位。</param>
  77. /// <exception cref="ArgumentNullException">cfg 为 null。</exception>
  78. /// <exception cref="ArgumentOutOfRangeException">LayerCount &lt;= 0。</exception>
  79. public static int[] ComputeLayerPositions(int focusZ, FocusLayerConfig cfg, int pulseMax = 0)
  80. {
  81. if (cfg == null) throw new ArgumentNullException(nameof(cfg));
  82. if (cfg.LayerCount <= 0)
  83. {
  84. throw new ArgumentOutOfRangeException(
  85. nameof(cfg), cfg.LayerCount, "LayerCount 必须 > 0");
  86. }
  87. int spacing = cfg.LayerSpacingPulse;
  88. // 对焦起点(第0层) = focusZ − down × spacing。
  89. int start = focusZ - cfg.LayerDown * spacing;
  90. var positions = new int[cfg.LayerCount];
  91. for (int i = 0; i < cfg.LayerCount; i++)
  92. {
  93. int z = start + i * spacing;
  94. if (pulseMax > 0)
  95. {
  96. // 越界钳位到 [0, pulseMax]。
  97. if (z < 0) z = 0;
  98. else if (z > pulseMax) z = pulseMax;
  99. }
  100. positions[i] = z;
  101. }
  102. return positions;
  103. }
  104. }
  105. }