autocal: 序列页阵列预览工具、2D 图点可见度,标签顺序与送到标定页

- 标签顺序:几何与安装 放到 测试序列 之前
- 序列点增加可选 uv_px(向后兼容),点表新增 U/V 列;加入所选点、删除所选、
  送到标定页(按 U/V 经同一套求解加入标定队列并切换页面)
- 已选点结果改显示在选点区状态行,不再写入全局顶部状态条
- 序列页左上区改紧凑:名称与备注同一行,分“阵列”“运动参数”两页;
  运动参数每行三组,输入框改用 compactField
- 新增阵列工具(array_plan.py、ui/array_tool.py):UV 规则网格(可蛇形遍历)与
  阶段预设,逐点检查是否在曲面内、是否留有边距,候选点实时预览在 2D 图上,
  只有可用点才能加入,已在序列里的点不重复加入
- 2D 平面图:标记改为缩放后再绘制并加深色描边,修复缩小后点不清;
  底图可选 原图/灰度/变暗/仅轮廓,点色可选 黄/白/青/橙/灰(默认灰度+黄,写入偏好);
  序列点带编号并连线显示遍历顺序,选中行高亮
- 新增测试 test_sequence_uv、test_sequence_to_capture、test_array_plan、test_array_tool
This commit is contained in:
ykcheungab
2026-10-19 11:39:09 +08:00
parent 566f1bf390
commit ff5f089dc1
17 changed files with 1602 additions and 125 deletions
+88
View File
@@ -0,0 +1,88 @@
"""Candidate arrays on the flat map: the grid, the presets and the on-surface check."""
from __future__ import annotations
from types import SimpleNamespace
import numpy as np
import pytest
from orisys_autocal import array_plan
from orisys_autocal.array_plan import (
NEAR_EDGE,
OFF_SURFACE,
PRESETS,
check_candidates,
grid_uv,
preset_uv,
)
def _flat(width: int = 200, height: int = 200, hole: tuple[int, int, int, int] | None = None):
"""A fully covered raster, optionally with a rectangular hole (x0, y0, x1, y1)."""
covered = np.ones((height, width), dtype=np.int32)
if hole:
x0, y0, x1, y1 = hole
covered[y0:y1, x0:x1] = -1
return SimpleNamespace(face_index=covered, width=width, height=height)
def test_a_grid_is_centred_and_bottom_row_first():
points = grid_uv(2, 3, 10, 20, centre=(0, 100), serpentine=False)
assert points == [(-10, 90), (0, 90), (10, 90), (-10, 110), (0, 110), (10, 110)]
def test_a_serpentine_walks_every_second_row_backwards():
points = grid_uv(2, 3, 10, 20, centre=(0, 100), serpentine=True)
assert points[3:] == [(10, 110), (0, 110), (-10, 110)]
def test_an_empty_or_oversized_grid():
assert grid_uv(0, 3, 10, 10) == []
with pytest.raises(ValueError, match="more than"):
grid_uv(30, 30, 1, 1)
def test_presets_overlap_only_at_the_hardware_check_point():
labels = [label for key in PRESETS for label, _u, _v in PRESETS[key]]
assert sorted(label for label in set(labels) if labels.count(label) > 1) == ["C13"]
# The first-model grid is centred on the hardware-check point.
assert ("C13", 0, 470) in PRESETS["stage1"]
assert ("C13", 0, 470) in PRESETS["stage0"]
assert preset_uv("stage0") == [("C13", 0, 470)]
with pytest.raises(ValueError):
preset_uv("nope")
assert array_plan.PRESET_KEYS[0] == "stage0"
def test_a_point_on_the_surface_is_valid_and_off_it_is_not():
flat = _flat(hole=(40, 40, 60, 60))
origin = (100, 150) # raster (x, y) = (100 + u, 150 - v)
result = check_candidates(flat, [(0, 100), (-50, 100)], origin=origin)
assert [c.valid for c in result] == [True, False]
assert result[1].reason == OFF_SURFACE # raster (50, 50) is in the hole
def test_outside_the_raster_is_off_surface_not_an_error():
result = check_candidates(_flat(), [(900, 900)], origin=(100, 150))
assert result[0].valid is False and result[0].reason == OFF_SURFACE
def test_a_margin_rejects_points_too_close_to_a_hole_or_the_border():
flat = _flat(hole=(100, 60, 110, 70))
origin = (100, 150)
# raster (100, 75): five pixels below the hole's bottom edge.
near = check_candidates(flat, [(0, 75)], margin_px=8, origin=origin)[0]
far = check_candidates(flat, [(0, 75)], margin_px=3, origin=origin)[0]
assert (near.valid, near.reason) == (False, NEAR_EDGE)
assert far.valid
# The raster border counts as an edge.
assert check_candidates(flat, [(-98, 100)], margin_px=5, origin=origin)[0].reason == NEAR_EDGE
def test_labels_survive_the_check_and_no_surface_vouches_for_nothing():
flat = _flat()
kept = check_candidates(flat, [("C13", 0, 100)], origin=(100, 150))[0]
assert kept.label == "C13" and kept.valid
none = check_candidates(None, [("C13", 0, 100), (1, 2)])
assert [c.valid for c in none] == [False, False]
assert none[0].label == "C13" and none[1].label == ""