修正朝上判断、接触高度与过期姿态目标

朝上判断误用平台轴、接触高度报旋转前数值、多点对准复用缓存姿态,且多点会话被复核入口拒绝;
现按修正计划改正并接通 autocal-multipoint-1.0 只读复核,缺失帧源如实报告而不补零。

离线验证:新回归在 dc5a6f8 上失败、此处通过,panel_check 通过,两截图点接触高度复现
19.0510 / 21.5781 mm;实机项未做,详见 .scratch/autocal-motion-fix/verification.md。
This commit is contained in:
ykcheungab
2026-10-15 06:52:28 +08:00
parent 5339624ffa
commit bcc55a3b97
29 changed files with 2179 additions and 209 deletions
+80 -3
View File
@@ -1168,7 +1168,11 @@ def test_the_sensor_z_offset_reaches_the_solve(panel):
panel.set_sensor_setup((0.0, 0.0, 4.0), 0.0)
thick = panel.stack.solve(point, normal)
assert thick.contact_z_mm - thin.contact_z_mm == pytest.approx(4.0)
# The mounting height rises by the four millimetres the setup asks for; the
# height the stages *present* rises by 4*cos(tilt) of that, because the offset
# is applied before the rotation.
assert thick.mounted_point_mm[2] - thin.mounted_point_mm[2] == pytest.approx(4.0)
assert 0.0 < thick.contact_z_mm - thin.contact_z_mm < 4.0
assert thick.target_xy_mm == pytest.approx(thin.target_xy_mm)
# Picking through the panel uses the same fields, so the two must agree.
panel.clear_points()
@@ -1179,7 +1183,8 @@ def test_the_sensor_z_offset_reaches_the_solve(panel):
thick_row = panel.handle_pick(x, y)
assert thin_row is not None and thick_row is not None
assert thick_row["residual"] == pytest.approx(thin_row["residual"], abs=1e-6)
assert thick_row["Z"] - thin_row["Z"] == pytest.approx(4.0)
assert thin_row["Z"] == pytest.approx(thin.contact_z_mm)
assert thick_row["Z"] == pytest.approx(thick.contact_z_mm)
assert thick_row["X"] == pytest.approx(thin_row["X"])
assert thick_row["Y"] == pytest.approx(thin_row["Y"])
@@ -1202,7 +1207,12 @@ def test_the_mounting_pose_moves_the_point_the_way_the_setup_says(panel):
assert moved.target_xy_mm[0] - base.target_xy_mm[0] == pytest.approx(2.0)
assert moved.target_xy_mm[1] - base.target_xy_mm[1] == pytest.approx(-3.0)
assert moved.contact_z_mm == pytest.approx(base.contact_z_mm)
# An in-plane offset leaves the mounting height alone but not the height the
# stages present: the same angles lift a contact that sits further from the
# trunnion axis. (This line used to compare contact_z_mm on both sides, which
# only held while that field was reported before the rotation.)
assert moved.mounted_point_mm[2] == pytest.approx(base.mounted_point_mm[2])
assert moved.contact_z_mm != pytest.approx(base.contact_z_mm)
# A yaw about the table's z turns the contact about the origin, so it moves the
# point and leaves its height alone.
@@ -1247,6 +1257,58 @@ def test_the_readouts_name_the_frame_mapping(panel):
assert "-y" in text or "−y" in text
def test_the_readout_names_the_chosen_pose_and_the_key_that_chose_it(panel):
"""The plan's rule: a branch is picked for a reason, and the bench sees which.
Both closed-form poses align a measured point; they differ by a half turn of
the body about the vertical. Whichever one the machine wants is a bench
convention, so the screen has to say which was taken and what decided it.
"""
from orisys_app_shared.i18n import t
x, y = _first_surface_pixel(panel.view.flat)
panel.handle_pick(x, y)
pose = panel._last_pose # noqa: SLF001
text = panel._readout_labels["branch"].text() # noqa: SLF001
assert pose.branch_reason in {"only", "alignment", "in_range", "upright", "smallest"}
assert t(f"finger.branch_{pose.branch_reason}") in text
# And the alternative's cost is on screen too, not just the chosen one's.
assert f"{pose.other_travel_deg:.1f}" in text
def test_the_press_readout_says_why_it_is_held_and_then_names_the_target(panel):
"""A geometric contact height is not a controller coordinate until it is measured."""
from orisys_app_shared.i18n import t
from orisys_autocal.motion.pose_move import PressAxis
x, y = _first_surface_pixel(panel.view.flat)
panel.handle_pick(x, y)
pose = panel._last_pose # noqa: SLF001
# The row records where the point *is*; the move records where the table goes.
height = pose.contact_z_mm
# Shipped state: no zero measured, so the press role is held — and the readout
# says so rather than showing a number the machine was never told about.
assert panel._readout_labels["press"].text() == t("finger.press_unset") # noqa: SLF001
targets, held = panel.solved_pose()
assert held == ("z",)
assert "z" not in targets
assert targets["x"] == pytest.approx(pose.table_xy_mm[0])
# With the bench's zero entered, the same contact height becomes a coordinate.
panel.set_press(PressAxis(zero_mm=2.0, rising=True, calibrated=True))
targets, held = panel.solved_pose()
assert held == ()
assert targets["z"] == pytest.approx(height + 2.0)
text = panel._readout_labels["press"].text() # noqa: SLF001
assert f"{height + 2.0:+.4f}" in text
# A downward-reading press axis flips the relation, and the readout follows.
panel.set_press(PressAxis(zero_mm=2.0, rising=False, calibrated=True))
targets, _held = panel.solved_pose()
assert targets["z"] == pytest.approx(2.0 - height)
def test_the_preview_refuses_to_draw_without_a_context(qt_app):
from orisys_autocal.ui.pose_preview import PosePreview
@@ -1613,3 +1675,18 @@ def test_the_surface_keeps_its_residual_colours_while_the_machine_moves(qt_app,
assert np.allclose(moved[index], residual_colour(preview._residuals[index])) # noqa: SLF001
finally:
preview.deleteLater()
def test_reapplying_the_current_theme_does_not_repolish_all_windows(make_window, qt_app, monkeypatch):
window = make_window()
calls = []
apply_style = qt_app.setStyleSheet
def record_style(style):
calls.append(style)
apply_style(style)
monkeypatch.setattr(qt_app, "setStyleSheet", record_style)
window._apply_theme()
assert calls == []
window._theme = "dark" if window._theme == "light" else "light"
window._apply_theme()
assert len(calls) == 1