PhotoLayerCalculator.cs 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  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. // 校验:下移层数须 < 层数,否则清晰层(focusZ 锚点=第 down 层)落在拍摄层数组之外。
  61. // 与 HouseDebugPageViewModel.ResolveEffectiveTune 手调路径校验对齐,使自动对焦链路同样兜底(非仅 ViewModel 手调)。
  62. if (down.Value >= count.Value)
  63. {
  64. throw new FocusConfigMissingException(
  65. raw.TlSn, raw.HouseSn, raw.WellSn,
  66. $"设备{raw.TlSn}对焦下移层数必须小于层数({count.Value}),否则清晰层落在拍摄范围外");
  67. }
  68. return new FocusLayerConfig
  69. {
  70. LayerSpacingPulse = spacing.Value,
  71. LayerCount = count.Value,
  72. LayerDown = down.Value,
  73. };
  74. }
  75. /// <summary>
  76. /// §2.4 层位置公式:
  77. /// 对焦起点(第0层) = focusZ − LayerDown × LayerSpacingPulse;
  78. /// 第 i 层 = 对焦起点 + i × LayerSpacingPulse (i = 0 .. LayerCount-1)。
  79. /// 返回各层绝对 Z 脉冲(长度 = LayerCount)。
  80. /// pulseMax &gt; 0 时对越界层钳位到 [0, pulseMax](对齐 verticalMotorPulseMax)。
  81. /// </summary>
  82. /// <param name="focusZ">对焦算出的最清晰层 Z 脉冲(锚点)。</param>
  83. /// <param name="cfg">已解析好的有效配置。</param>
  84. /// <param name="pulseMax">垂直电机脉冲上限;&lt;=0 表示不钳位。</param>
  85. /// <exception cref="ArgumentNullException">cfg 为 null。</exception>
  86. /// <exception cref="ArgumentOutOfRangeException">LayerCount &lt;= 0。</exception>
  87. public static int[] ComputeLayerPositions(int focusZ, FocusLayerConfig cfg, int pulseMax = 0)
  88. {
  89. if (cfg == null) throw new ArgumentNullException(nameof(cfg));
  90. if (cfg.LayerCount <= 0)
  91. {
  92. throw new ArgumentOutOfRangeException(
  93. nameof(cfg), cfg.LayerCount, "LayerCount 必须 > 0");
  94. }
  95. int spacing = cfg.LayerSpacingPulse;
  96. // 对焦起点(第0层) = focusZ − down × spacing。
  97. int start = focusZ - cfg.LayerDown * spacing;
  98. var positions = new int[cfg.LayerCount];
  99. for (int i = 0; i < cfg.LayerCount; i++)
  100. {
  101. int z = start + i * spacing;
  102. if (pulseMax > 0)
  103. {
  104. // 越界钳位到 [0, pulseMax]。
  105. if (z < 0) z = 0;
  106. else if (z > pulseMax) z = pulseMax;
  107. }
  108. positions[i] = z;
  109. }
  110. return positions;
  111. }
  112. }
  113. }