@@ -1,8 +1,7 @@
""" The finger debugger ' s panel: picking, refusing, and what a row contains.
The bundle is the real export on this machine. It is skipped rather than failed
w hen absent, because the export lives in a sibling design repo: a machine without
it has not broken AutoCal.
Interaction tests use a small dome. Geometry and native-resolution checks keep
t he real export and skip individually when that optional export is unavailable.
"""
from __future__ import annotations
@@ -11,27 +10,26 @@ from pathlib import Path
import numpy as np
import pytest
from orisys_autocal . motion . kinematics import PlatformStack
from orisys_autocal . motion . kinematics import PlatformStack , rotation_about
from orisys_autocal . sensor import (
DEFAULT_FLAT_SIZE ,
load_bundle ,
rasterise ,
to_sensor ,
)
from orisys_autocal . sensor . export_bundle import SensorBundle
from orisys_autocal . ui . sensor_panel import FingerPanel
from orisys_autocal . ui . pose_preview import PosePreview
from PySide6 . QtCore import QPointF
BUNDLE = Path (
r " D: \ orisys_ws \ orisys-sensor-design \ build \ export \ finger \ r01 \ compute "
)
pytestmark = pytest . mark . skipif (
not BUNDLE . is_dir ( ) , reason = f " no sensor export at { BUNDLE } "
)
@pytest.fixture ( scope = " module " )
def bundle ( ) :
if not BUNDLE . is_dir ( ) :
pytest . skip ( f " no sensor export at { BUNDLE } " )
return load_bundle ( BUNDLE )
@@ -40,11 +38,44 @@ def flat(bundle):
return rasterise ( bundle , size_hw = DEFAULT_FLAT_SIZE )
@pytest.fixture ( scope = " module " )
def small_surface ( ) :
""" A five-vertex dome with tilted normals and empty UV corners for UI tests. """
vertices = np . array ( [ [ - 5 , 8 , 8 ] , [ 5 , 8 , 8 ] , [ - 5 , 8 , 18 ] , [ 5 , 8 , 18 ] , [ 0 , 12 , 13 ] ] , dtype = float )
faces = np . array ( [ [ 0 , 4 , 1 ] , [ 1 , 4 , 3 ] , [ 3 , 4 , 2 ] , [ 2 , 4 , 0 ] ] )
uv = np . array ( [ [ .5 , 0 ] , [ 1 , .5 ] , [ 0 , .5 ] , [ .5 , 1 ] , [ .5 , .5 ] ] )
surface = SensorBundle ( path = Path ( " synthetic-finger " ) , vertices_mm = vertices , faces = faces ,
uv_data = uv [ faces ] . reshape ( - 1 , 2 ) , loop_to_vertex = faces . ravel ( ) )
return surface , rasterise ( surface , size_hw = ( 128 , 128 ) )
@pytest.fixture
def panel ( qt_app , bundle , flat ) :
def panel ( qt_app , small_surface ) :
widget = FingerPanel ( stack = PlatformStack ( ) )
widget . set_bundle ( * small_surface )
yield widget
widget . preview . stop_simulation ( )
widget . close ( )
widget . deleteLater ( )
@pytest.fixture
def native_panel ( qt_app , bundle , flat ) :
""" Keep the production mesh and raster for native-resolution mapping checks. """
widget = FingerPanel ( stack = PlatformStack ( ) )
widget . set_bundle ( bundle , flat )
yield widget
widget . preview . stop_simulation ( )
widget . close ( )
widget . deleteLater ( )
@pytest.fixture
def preview ( qt_app ) :
widget = PosePreview ( )
yield widget
widget . stop_simulation ( )
widget . close ( )
widget . deleteLater ( )
@@ -71,7 +102,7 @@ def _shrink_raster(monkeypatch, window_mod, size=(256, 256)):
monkeypatch . setattr ( window_mod , " rasterise " , small )
def test_the_window_loads_the_remembered_bundle_at_startup ( qt_app , monkeypatch ) :
def test_the_window_loads_the_remembered_bundle_at_startup ( qt_app , bundle , monkeypatch ) :
""" A filled-in path over two empty views reads as a broken screen. """
import shutil
@@ -139,7 +170,7 @@ def test_the_preview_legend_is_a_label_not_gl_text(qt_app):
# ------------------------------------------------- the initial view and axes
def test_the_initial_view_is_the_commissioned_orientation ( qt_app , bundle ) :
def test_the_initial_view_is_the_commissioned_orientation ( bundle , preview ) :
""" Sensor +Y up the screen and sensor +Z toward the viewer, by construction.
The user asked for this state to be the configuration the screen opens in, so
@@ -149,10 +180,7 @@ def test_the_initial_view_is_the_commissioned_orientation(qt_app, bundle):
right, because getting it wrong draws the tool axis pointing down.
"""
from orisys_autocal . sensor import to_sensor
from orisys_autocal . ui . pose_preview import PosePreview
preview = PosePreview ( )
try :
preview . resize ( 600 , 400 )
preview . set_bundle ( bundle )
preview . initial_view ( )
@@ -177,19 +205,11 @@ def test_the_initial_view_is_the_commissioned_orientation(qt_app, bundle):
assert eye [ 1 ] > centre [ 1 ]
assert eye [ 2 ] < centre [ 2 ]
assert forward [ 1 ] < - 0.5 , f " the view must look toward − Y, but forward is { forward } "
finally :
preview . deleteLater ( )
def test_orbiting_and_then_resetting_returns_to_the_initial_view ( qt_app , bundle ) :
from orisys_autocal . ui . pose_preview import (
INITIAL_PITCH_RAD ,
INITIAL_YAW_RAD ,
PosePreview ,
)
def test_orbiting_and_then_resetting_returns_to_the_initial_view ( bundle , preview ) :
from orisys_autocal . ui . pose_preview import INITIAL_PITCH_RAD , INITIAL_YAW_RAD
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
preview . _yaw = 1.9 # noqa: SLF001 - pretend the operator dragged
preview . _pitch = - 0.4 # noqa: SLF001
@@ -200,31 +220,21 @@ def test_orbiting_and_then_resetting_returns_to_the_initial_view(qt_app, bundle)
assert preview . _yaw == pytest . approx ( INITIAL_YAW_RAD ) # noqa: SLF001
assert preview . _pitch == pytest . approx ( INITIAL_PITCH_RAD ) # noqa: SLF001
assert preview . _distance_scale == pytest . approx ( 1.0 ) # noqa: SLF001
finally :
preview . deleteLater ( )
def test_the_motion_axes_can_be_turned_off ( qt_app , bundle ) :
from orisys_autocal . ui . pose_preview import PosePreview
def test_the_motion_axes_can_be_turned_off ( bundle , preview ) :
preview = PosePreview ( )
try :
assert preview . show_axes
preview . set_show_axes ( False )
assert not preview . show_axes
preview . set_show_axes ( True )
assert preview . show_axes
finally :
preview . deleteLater ( )
def test_the_axis_labels_project_onto_their_arrows ( qt_app , bundle ) :
def test_the_axis_labels_project_onto_their_arrows ( bundle , preview ) :
""" The 2D label and the 3D arrow must agree, or the label names nothing. """
from orisys_autocal . sensor import to_sensor
from orisys_autocal . ui . pose_preview import PosePreview
preview = PosePreview ( )
try :
preview . resize ( 600 , 400 )
preview . set_bundle ( bundle )
stack = PlatformStack ( )
@@ -257,20 +267,13 @@ def test_the_axis_labels_project_onto_their_arrows(qt_app, bundle):
for x , y in clamped :
assert 0 < = x < = preview . width ( )
assert 0 < = y < = preview . height ( )
finally :
preview . deleteLater ( )
def test_a_point_behind_the_camera_has_no_screen_position ( qt_app , bundle ) :
def test_a_point_behind_the_camera_has_no_screen_position ( bundle , preview ) :
""" Drawing a label for it would put a stray letter on the widget. """
from orisys_autocal . ui . pose_preview import PosePreview
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
assert preview . project ( bundle . centre_mm ) is not None
finally :
preview . deleteLater ( )
def _frustum_pixel ( preview , point_mm ) - > tuple [ float , float ] :
@@ -313,12 +316,9 @@ def _frustum_pixel(preview, point_mm) -> tuple[float, float]:
return ( ( ndc_x + 1.0 ) / 2.0 * width , ( 1.0 - ndc_y ) / 2.0 * height )
def test_the_projection_matches_the_frustum_it_is_drawn_with ( qt_app , bundle ) :
def test_the_projection_matches_the_frustum_it_is_drawn_with ( bundle , preview ) :
""" The overlay is placed with this, so it has to be the renderer ' s projection. """
from orisys_autocal . ui . pose_preview import PosePreview
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
preview . resize ( 900 , 640 )
stack = PlatformStack ( )
@@ -339,13 +339,11 @@ def test_the_projection_matches_the_frustum_it_is_drawn_with(qt_app, bundle):
# must agree on — the check that survives any reformulation of either side.
mid = preview . project ( preview . _focus ( ) ) # noqa: SLF001
assert mid == pytest . approx ( ( 450.0 , 320.0 ) , abs = 1e-6 )
finally :
preview . deleteLater ( )
def test_the_flat_image_is_shown_whole_not_cropped ( panel ) :
def test_the_flat_image_is_shown_whole_not_cropped ( native_panel ) :
""" At native size the 1024 image was cropped to a corner of the UV space. """
view = panel . view
view = native_panel . view
view . resize ( 240 , 300 )
view . set_flat ( view . flat )
@@ -357,9 +355,9 @@ def test_the_flat_image_is_shown_whole_not_cropped(panel):
assert shown . width ( ) == pytest . approx ( shown . height ( ) , abs = 2 )
def test_a_click_on_the_scaled_image_maps_to_the_pixel_under_it ( panel ) :
def test_a_click_on_the_scaled_image_maps_to_the_pixel_under_it ( native_panel ) :
""" Scaling must not move the mapping; a click in the corner is the corner. """
view = panel . view
view = native_panel . view
view . resize ( 240 , 300 )
view . set_flat ( view . flat )
shown = view . pixmap ( )
@@ -417,7 +415,7 @@ def _run_simulation(preview) -> None:
preview . _sim_tick ( ) # noqa: SLF001 - stepping the animation deterministically
def test_playing_the_motion_puts_the_normal_on_the_tool_axis ( qt_app , bundle ) :
def test_playing_the_motion_puts_the_normal_on_the_tool_axis ( bundle , preview ) :
""" The two invariants the whole animation exists to show.
After playback the picked normal must lie along the tool axis, and the contact
@@ -426,14 +424,11 @@ def test_playing_the_motion_puts_the_normal_on_the_tool_axis(qt_app, bundle):
and checking against it made the test agree with the drawing and with nothing
on the bench.
"""
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
tool_local = to_sensor ( np . asarray ( stack . tool_direction , dtype = float ) , stack . axis_map )
tool_local = tool_local / np . linalg . norm ( tool_local )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
normals = bundle . vertex_normals ( )
worst_angle = 0.0
@@ -480,22 +475,16 @@ def test_playing_the_motion_puts_the_normal_on_the_tool_axis(qt_app, bundle):
assert worst_angle < 0.5 , (
f " after playing, a normal is still { worst_angle : .3f } deg off the tool axis "
)
finally :
preview . stop_simulation ( )
preview . deleteLater ( )
def test_the_simulation_rotates_first_and_then_translates ( qt_app , bundle ) :
def test_the_simulation_rotates_first_and_then_translates ( bundle , preview ) :
""" The bench contract: stages settle before the table moves. """
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
surface_normal = bundle . vertex_normals ( ) [ 0 ]
point = bundle . vertices_mm [ 0 ]
pose = stack . solve ( point , surface_normal )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
preview . set_point ( point , surface_normal )
preview . set_pose ( pose )
@@ -516,19 +505,13 @@ def test_the_simulation_rotates_first_and_then_translates(qt_app, bundle):
rotation_share , translation_share = preview . simulation_phases ( ) # noqa: SLF001
assert rotation_share == pytest . approx ( 1.0 )
assert translation_share == pytest . approx ( 1.0 )
finally :
preview . stop_simulation ( )
preview . deleteLater ( )
def test_the_simulation_stops_and_can_be_replayed ( qt_app , bundle ) :
from orisys_autocal . ui . pose_preview import PosePreview
def test_the_simulation_stops_and_can_be_replayed ( bundle , preview ) :
stack = PlatformStack ( )
point = bundle . vertices_mm [ 0 ]
pose = stack . solve ( point , bundle . vertex_normals ( ) [ 0 ] )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
preview . set_point ( point , bundle . vertex_normals ( ) [ 0 ] )
preview . set_pose ( pose )
@@ -545,23 +528,15 @@ def test_the_simulation_stops_and_can_be_replayed(qt_app, bundle):
assert not preview . simulating
preview . start_simulation ( )
assert preview . simulating
finally :
preview . stop_simulation ( )
preview . deleteLater ( )
def test_the_simulation_cannot_start_without_a_pose ( qt_app , bundle ) :
from orisys_autocal . ui . pose_preview import PosePreview
def test_the_simulation_cannot_start_without_a_pose ( bundle , preview ) :
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
assert not preview . can_simulate
preview . start_simulation ( )
assert not preview . simulating
assert preview . _sim_transform ( ) is None # noqa: SLF001
finally :
preview . deleteLater ( )
# ------------------------------------------------------------- the point table
@@ -720,7 +695,7 @@ def test_clearing_the_points_removes_the_markers(panel):
# ------------------------------------------------------- the motion table frame
def test_the_two_rotation_axes_cross_at_one_origin ( qt_app , bundle ) :
def test_the_two_rotation_axes_cross_at_one_origin ( bundle , preview ) :
""" The bench statement: yaw and pitch share an origin.
Both axes are drawn through the frame ' s single ``origin`` point, so if this
@@ -728,10 +703,7 @@ def test_the_two_rotation_axes_cross_at_one_origin(qt_app, bundle):
asserted on the returned frame rather than the drawing, because a picture
cannot be checked in a test and a wrong frame would draw perfectly happily.
"""
from orisys_autocal . ui . pose_preview import PosePreview
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
frame = preview . table_frame ( )
origin = frame [ " origin " ]
@@ -745,21 +717,16 @@ def test_the_two_rotation_axes_cross_at_one_origin(qt_app, bundle):
assert np . allclose ( offset , 0.0 )
# And the two axes are distinct, not accidentally parallel.
assert abs ( float ( frame [ " yaw_direction " ] @ frame [ " pitch_direction " ] ) ) < 1e-6
finally :
preview . deleteLater ( )
def test_the_sensor_base_sits_above_the_origin_by_the_setup_z ( qt_app , bundle ) :
def test_the_sensor_base_sits_above_the_origin_by_the_setup_z ( bundle , preview ) :
""" The other half of the statement, and the reason for the highlighted plane.
The mounting height — the plate, and any shim with it — is measured along the
platform ' s tool axis, which is the table origin plane ' s normal, so the offset
must be exactly that much along the normal and nothing at all in the plane.
"""
from orisys_autocal . ui . pose_preview import PosePreview
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
for plate in ( 0.0 , 2.5 , 6.0 ) :
preview . set_setup ( ( 0.0 , 0.0 , plate ) , 0.0 )
@@ -771,16 +738,11 @@ def test_the_sensor_base_sits_above_the_origin_by_the_setup_z(qt_app, bundle):
# No sideways component, so the base plane is parallel to the origin
# plane rather than merely offset from it.
assert float ( np . linalg . norm ( gap - ( gap @ normal ) * normal ) ) == pytest . approx ( 0.0 )
finally :
preview . deleteLater ( )
def test_the_table_frame_normal_is_the_tool_axis ( qt_app , bundle ) :
def test_the_table_frame_normal_is_the_tool_axis ( bundle , preview ) :
""" The origin plane ' s normal is the direction the sensor base is measured along. """
from orisys_autocal . ui . pose_preview import PosePreview
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
frame = preview . table_frame ( )
# In the mesh's frame, so compare against the tool axis mapped the same way.
@@ -789,20 +751,15 @@ def test_the_table_frame_normal_is_the_tool_axis(qt_app, bundle):
)
expected = expected / np . linalg . norm ( expected )
assert np . allclose ( frame [ " plane_normal " ] , expected , atol = 1e-12 )
finally :
preview . deleteLater ( )
def test_the_table_frame_does_not_move_with_the_simulation ( qt_app , bundle ) :
def test_the_table_frame_does_not_move_with_the_simulation ( bundle , preview ) :
""" It is the machine ' s frame: it must stay put while the sensor is animated.
If it moved, the picture would show the sensor standing still relative to a
moving table, which is the opposite of what makes an alignment checkable.
"""
from orisys_autocal . ui . pose_preview import PosePreview
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
before = { key : np . array ( value ) for key , value in preview . table_frame ( ) . items ( ) }
point = bundle . vertices_mm [ 0 ]
@@ -813,23 +770,15 @@ def test_the_table_frame_does_not_move_with_the_simulation(qt_app, bundle):
after = preview . table_frame ( )
for key , value in before . items ( ) :
assert np . allclose ( after [ key ] , value ) , f " { key } moved with the simulation "
finally :
preview . stop_simulation ( )
preview . deleteLater ( )
def test_the_table_frame_can_be_hidden ( qt_app , bundle ) :
from orisys_autocal . ui . pose_preview import PosePreview
def test_the_table_frame_can_be_hidden ( bundle , preview ) :
preview = PosePreview ( )
try :
assert preview . show_table
preview . set_show_table ( False )
assert not preview . show_table
preview . set_show_table ( True )
assert preview . show_table
finally :
preview . deleteLater ( )
def test_the_panel_drives_the_base_plane_from_the_sensor_z_field ( panel ) :
@@ -845,21 +794,18 @@ def test_the_panel_drives_the_base_plane_from_the_sensor_z_field(panel):
# ------------------------------------------------------------- the tool path
def test_the_planned_path_starts_at_the_contact_and_ends_on_the_tool ( qt_app , bundle ) :
def test_the_planned_path_starts_at_the_contact_and_ends_on_the_tool ( bundle , preview ) :
""" The two ends of the trajectory are the two things it claims.
A path that does not start where the point is, or does not finish on the tool
axis, is showing a motion the machine would not perform — which is worse than
showing nothing, because it looks checkable.
"""
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
axis = to_sensor ( stack . tool_axis_direction , stack . axis_map )
axis = axis / np . linalg . norm ( axis )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
for index in ( 0 , 7 , 40 , 200 ) :
point , normal = bundle . vertices_mm [ index ] , bundle . vertex_normals ( ) [ index ]
@@ -878,11 +824,9 @@ def test_the_planned_path_starts_at_the_contact_and_ends_on_the_tool(qt_app, bun
# Continuous: no step larger than the body, or it is not a path.
steps = np . linalg . norm ( np . diff ( path , axis = 0 ) , axis = 1 )
assert float ( steps . max ( ) ) < float ( np . linalg . norm ( bundle . extent_mm ) )
finally :
preview . deleteLater ( )
def test_the_path_ends_where_the_pose_says_the_table_moves_to ( qt_app , bundle ) :
def test_the_path_ends_where_the_pose_says_the_table_moves_to ( bundle , preview ) :
""" The path, the animation and the reported move are one claim, checked once.
The contact ' s last drawn position must be the tool, which is the tool ' s own XY —
@@ -890,15 +834,12 @@ def test_the_path_ends_where_the_pose_says_the_table_moves_to(qt_app, bundle):
somewhere near it. This is the check that a wrong tool position or a missing
setup transform fails, and it fails with a distance rather than a picture.
"""
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack (
tool_xy_mm = ( 6.0 , - 3.0 ) ,
sensor_offset_mm = ( 1.5 , - 2.0 , 4.0 ) ,
sensor_yaw_deg = 7.5 ,
)
preview = PosePreview ( )
try :
preview . set_stack ( stack )
preview . set_bundle ( bundle )
preview . set_setup ( stack . sensor_offset_mm , stack . sensor_yaw_deg )
@@ -921,23 +862,17 @@ def test_the_path_ends_where_the_pose_says_the_table_moves_to(qt_app, bundle):
moved = stack . mount @ np . asarray ( centre , dtype = float )
assert moved [ 0 ] == pytest . approx ( pose . table_xy_mm [ 0 ] , abs = 1e-6 )
assert moved [ 1 ] == pytest . approx ( pose . table_xy_mm [ 1 ] , abs = 1e-6 )
finally :
preview . stop_simulation ( )
preview . deleteLater ( )
def test_the_table_marker_shows_the_move_against_the_fixed_grid ( qt_app , bundle ) :
def test_the_table_marker_shows_the_move_against_the_fixed_grid ( bundle , preview ) :
""" Item three of the fidelity request: what the machine did, versus where it started.
The grid is the initial state and never moves. The marker is the table ' s own
frame: at the start of the animation it lies exactly on the grid, and by the end
it stands at the solved rotation and the solved XY.
"""
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
preview = PosePreview ( )
try :
preview . set_stack ( stack )
preview . set_bundle ( bundle )
point , normal = bundle . vertices_mm [ 40 ] , bundle . vertex_normals ( ) [ 40 ]
@@ -962,17 +897,12 @@ def test_the_table_marker_shows_the_move_against_the_fixed_grid(qt_app, bundle):
moved = stack . mount @ np . asarray ( translation , dtype = float )
assert moved [ 0 ] == pytest . approx ( pose . table_xy_mm [ 0 ] , abs = 1e-9 )
assert moved [ 1 ] == pytest . approx ( pose . table_xy_mm [ 1 ] , abs = 1e-9 )
finally :
preview . deleteLater ( )
def test_the_path_is_a_rotation_arc_then_a_straight_move ( qt_app , bundle ) :
def test_the_path_is_a_rotation_arc_then_a_straight_move ( bundle , preview ) :
""" Two phases, matching the two things the machine has. """
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
point , normal = bundle . vertices_mm [ 40 ] , bundle . vertex_normals ( ) [ 40 ]
preview . set_point ( point , normal )
@@ -994,17 +924,12 @@ def test_the_path_is_a_rotation_arc_then_a_straight_move(qt_app, bundle):
directions = segments [ keep ] / lengths [ keep ] [ : , None ]
for first , second in zip ( directions , directions [ 1 : ] ) :
assert float ( first @ second ) > 0.99 , " the table move is not straight "
finally :
preview . deleteLater ( )
def test_the_path_summary_reports_what_judges_validity ( qt_app , bundle ) :
def test_the_path_summary_reports_what_judges_validity ( bundle , preview ) :
""" The numbers, not just the line: reach and arc radius. """
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
assert preview . trajectory_summary ( ) == { }
point , normal = bundle . vertices_mm [ 0 ] , bundle . vertex_normals ( ) [ 0 ]
@@ -1018,33 +943,23 @@ def test_the_path_summary_reports_what_judges_validity(qt_app, bundle):
float ( np . linalg . norm ( point - origin ) )
)
assert summary [ " final_offset_mm " ] == pytest . approx ( 0.0 , abs = 1e-6 )
finally :
preview . deleteLater ( )
def test_the_path_can_be_hidden ( qt_app , bundle ) :
from orisys_autocal . ui . pose_preview import PosePreview
def test_the_path_can_be_hidden ( bundle , preview ) :
preview = PosePreview ( )
try :
assert preview . show_trajectory
preview . set_show_trajectory ( False )
assert not preview . show_trajectory
finally :
preview . deleteLater ( )
def test_the_origin_plane_and_the_base_plane_are_separate_surfaces ( qt_app , bundle ) :
def test_the_origin_plane_and_the_base_plane_are_separate_surfaces ( bundle , preview ) :
""" They coincide only when the mounting height is zero; the frame must say so.
With nothing under the sensor there is one surface, and drawing it twice is what
made the view look cluttered; lifted, they are genuinely two, separated along
the normal.
"""
from orisys_autocal . ui . pose_preview import PosePreview
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
preview . set_setup ( ( 0.0 , 0.0 , 0.0 ) , 0.0 )
frame = preview . table_frame ( )
@@ -1055,16 +970,11 @@ def test_the_origin_plane_and_the_base_plane_are_separate_surfaces(qt_app, bundl
gap = frame [ " base_point " ] - frame [ " origin " ]
assert float ( gap @ frame [ " plane_normal " ] ) == pytest . approx ( 3.0 )
assert not np . allclose ( frame [ " origin " ] , frame [ " base_point " ] )
finally :
preview . deleteLater ( )
def test_the_table_triad_basis_is_well_defined ( qt_app , bundle ) :
def test_the_table_triad_basis_is_well_defined ( bundle , preview ) :
""" A degenerate cross product would orient the plane arbitrarily. """
from orisys_autocal . ui . pose_preview import PosePreview
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
frame = preview . table_frame ( )
normal , first , second = frame [ " plane_normal " ] , frame [ " first " ] , frame [ " second " ]
@@ -1073,8 +983,6 @@ def test_the_table_triad_basis_is_well_defined(qt_app, bundle):
assert float ( first @ normal ) == pytest . approx ( 0.0 , abs = 1e-12 )
assert float ( second @ normal ) == pytest . approx ( 0.0 , abs = 1e-12 )
assert float ( first @ second ) == pytest . approx ( 0.0 , abs = 1e-12 )
finally :
preview . deleteLater ( )
def test_a_pick_returns_the_row_it_also_shows ( panel ) :
@@ -1184,13 +1092,14 @@ def test_the_sensor_z_offset_reaches_the_solve(panel):
assert thick_row [ " Y " ] == pytest . approx ( thin_row [ " Y " ] )
def test_the_mounting_pose_moves_the_point_the_way_the_setup_says ( panel ) :
def test_the_mounting_pose_moves_the_point_the_way_the_setup_says ( native_panel ) :
""" x, y and yaw are a mounting pose, so they move the contact, not the reading.
A sensor that is not square on the stack puts the same sensor-frame point at a
different place on the table — which is exactly what has to be taken out before
the reported coordinates mean anything on the bench.
"""
panel = native_panel
flat = panel . view . flat
x , y = _first_surface_pixel ( flat )
point , normal , _face = flat . sample ( x , y )
@@ -1217,7 +1126,15 @@ def test_the_mounting_pose_moves_the_point_the_way_the_setup_says(panel):
assert float ( np . hypot ( * turned . target_xy_mm ) ) == pytest . approx ( radius , rel = 1e-9 )
assert turned . contact_z_mm == pytest . approx ( base . contact_z_mm )
assert turned . yaw_deg != pytest . approx ( base . yaw_deg )
assert turned . pitch_deg == pytest . approx ( base . pitch_deg )
# The shorter equivalent branch can change pitch's sign. Compare the full
# rotation, including the changed mounting yaw, against the original pose.
relative = ( panel . stack . platform_rotation ( turned . yaw_deg , turned . pitch_deg )
@ rotation_about ( " z " , 30.0 )
@ panel . stack . platform_rotation ( base . yaw_deg , base . pitch_deg ) . T )
assert ( np . allclose ( relative , np . eye ( 3 ) , atol = 1e-9 )
or np . allclose ( relative , rotation_about ( " z " , 180.0 ) , atol = 1e-9 ) )
assert np . allclose ( panel . stack . platform_rotation ( turned . yaw_deg , turned . pitch_deg )
@ panel . stack . setup_normal ( normal ) , panel . stack . tool_direction , atol = 1e-9 )
def test_the_tool_position_sets_the_move_and_not_the_reading ( panel ) :
@@ -1306,11 +1223,8 @@ def test_the_press_readout_says_why_it_is_held_and_then_names_the_target(panel):
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
def test_the_preview_refuses_to_draw_without_a_context ( preview ) :
preview = PosePreview ( )
try :
assert not preview . has_context ( )
# Rendering must be a no-op, not a crash: Qt calls this during teardown
# on some platforms.
@@ -1319,8 +1233,6 @@ def test_the_preview_refuses_to_draw_without_a_context(qt_app):
# leaving a black rectangle unexplained.
preview . set_bundle ( None )
assert preview . problem
finally :
preview . deleteLater ( )
def test_the_panel_shows_why_the_preview_is_blank ( qt_app ) :
@@ -1378,7 +1290,7 @@ def test_every_palette_colour_can_be_set_on_a_strict_gl(qt_app):
# --------------------------------------------- the camera frames the whole scene
def test_the_whole_scene_fits_the_widget_at_the_default_zoom ( qt_app , bundle ) :
def test_the_whole_scene_fits_the_widget_at_the_default_zoom ( bundle , preview ) :
""" Every point the framing counts on must land inside the pane.
Framed on the mesh alone — half its bounding-box diagonal, times three — the
@@ -1386,11 +1298,8 @@ def test_the_whole_scene_fits_the_widget_at_the_default_zoom(qt_app, bundle):
two things that say whether a pose is reachable. A pose whose parts leave the
picture looks like a pose that failed.
"""
from orisys_autocal . ui . pose_preview import PosePreview
width , height = 900 , 640
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
preview . resize ( width , height )
points = preview . scene_points ( )
@@ -1400,11 +1309,9 @@ def test_the_whole_scene_fits_the_widget_at_the_default_zoom(qt_app, bundle):
assert screen is not None , " a scene point projected behind the camera "
assert - 1.0 < = screen [ 0 ] < = width + 1.0 , f " x= { screen [ 0 ] : .1f } outside 0.. { width } "
assert - 1.0 < = screen [ 1 ] < = height + 1.0 , f " y= { screen [ 1 ] : .1f } outside 0.. { height } "
finally :
preview . deleteLater ( )
def test_the_resting_view_shows_the_solved_end_state ( qt_app , bundle ) :
def test_the_resting_view_shows_the_solved_end_state ( bundle , preview ) :
""" At rest the screen answers " where does it end up " without an animation.
This is a solve view: the operator picks a point and needs to see whether the
@@ -1412,11 +1319,8 @@ def test_the_resting_view_shows_the_solved_end_state(qt_app, bundle):
the table marker all stand at the end of the move before anything is played, and
the fixed grid is the frame they started in.
"""
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( tool_xy_mm = ( 5.0 , - 2.0 ) )
preview = PosePreview ( )
try :
preview . set_stack ( stack )
preview . set_bundle ( bundle )
preview . set_tool_xy ( * stack . tool_xy_mm )
@@ -1439,17 +1343,12 @@ def test_the_resting_view_shows_the_solved_end_state(qt_app, bundle):
start = preview . initial_marker_points ( )
assert np . allclose ( start . mean ( axis = 0 ) , [ 0.0 , 0.0 , 0.0 ] , atol = 1e-9 )
assert not np . allclose ( start , preview . table_marker_points ( ) , atol = 1e-6 )
finally :
preview . deleteLater ( )
def test_playing_from_the_resting_state_moves_the_body ( qt_app , bundle ) :
def test_playing_from_the_resting_state_moves_the_body ( bundle , preview ) :
""" Play runs the move the resting picture already shows the end of. """
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
index = 40
point , normal = bundle . vertices_mm [ index ] , bundle . vertex_normals ( ) [ index ]
@@ -1465,27 +1364,21 @@ def test_playing_from_the_resting_state_moves_the_body(qt_app, bundle):
for _ in range ( 200 ) :
preview . _sim_tick ( ) # noqa: SLF001
assert np . allclose ( preview . _placed_contact ( point ) , resting , atol = 1e-6 ) # noqa: SLF001
finally :
preview . stop_simulation ( )
preview . deleteLater ( )
def test_the_landing_check_agrees_with_the_tool ( qt_app , bundle ) :
def test_the_landing_check_agrees_with_the_tool ( bundle , preview ) :
""" The x-y claim, measured rather than judged off a picture.
``landing_xy_mm`` pushes the contact through the *drawing ' s* transform; the tool ' s
X/Y is a setup number. They are computed two different ways and must agree — this
is the check the bench asks for when it says " x and y are not aligning " .
"""
from orisys_autocal . ui . pose_preview import PosePreview
setup = PlatformStack (
sensor_offset_mm = ( 6.0 , - 3.0 , 5.0 ) ,
sensor_yaw_deg = 12.0 ,
tool_xy_mm = ( 7.0 , 4.0 ) ,
)
preview = PosePreview ( )
try :
preview . set_stack ( setup )
preview . set_bundle ( bundle )
preview . set_setup ( setup . sensor_offset_mm , setup . sensor_yaw_deg )
@@ -1502,8 +1395,6 @@ def test_the_landing_check_agrees_with_the_tool(qt_app, bundle):
assert landing is not None
worst = max ( worst , float ( np . hypot ( landing [ 0 ] - 7.0 , landing [ 1 ] - 4.0 ) ) )
assert worst < 1e-9 , f " the drawing lands { worst * 1000 : .3f } um off the tool "
finally :
preview . deleteLater ( )
def test_the_landing_line_reports_the_check ( panel ) :
@@ -1528,7 +1419,7 @@ def test_the_landing_line_says_so_when_they_disagree(panel):
assert " 0.250 " in panel . path_label . text ( )
def test_the_framing_follows_the_planned_path ( qt_app , bundle ) :
def test_the_framing_follows_the_planned_path ( bundle , preview ) :
""" A long move has to fit in the picture, or the move leaves the frame.
The body at rest stands at the *end* of the move, so the far end of the picture
@@ -1536,11 +1427,8 @@ def test_the_framing_follows_the_planned_path(qt_app, bundle):
points, which is what this checks. (It used to be enough to compare the radius
with and without a pose, because the body itself was at the far end.)
"""
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
preview . resize ( 900 , 640 )
@@ -1559,25 +1447,20 @@ def test_the_framing_follows_the_planned_path(qt_app, bundle):
start = np . linalg . norm ( path [ 0 ] - preview . _focus ( ) ) # noqa: SLF001
assert radius > = start - 1e-9
assert start > 0.0
finally :
preview . deleteLater ( )
# ------------------------------------ the animation and the drawn path agree
def test_the_drawn_path_ends_where_the_animation_puts_the_contact ( qt_app , bundle ) :
def test_the_drawn_path_ends_where_the_animation_puts_the_contact ( bundle , preview ) :
""" Two descriptions of one move, so they cannot disagree.
The path was sampled up to ``SIM_ROTATION_SHARE`` of the solved rotation — the
*time* that phase takes, not how far it turns — so the drawn arc stopped 45 %
short and the straight move began from a point the machine never passes through.
"""
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
index = 7
point , normal = bundle . vertices_mm [ index ] , bundle . vertex_normals ( ) [ index ]
@@ -1590,22 +1473,17 @@ def test_the_drawn_path_ends_where_the_animation_puts_the_contact(qt_app, bundle
_run_simulation ( preview )
landed = preview . _placed_contact ( point ) # noqa: SLF001
assert np . allclose ( path [ - 1 ] , landed , atol = 1e-6 )
finally :
preview . deleteLater ( )
def test_the_placement_covers_a_point_and_a_whole_vertex_array ( qt_app , bundle ) :
def test_the_placement_covers_a_point_and_a_whole_vertex_array ( bundle , preview ) :
""" ``_placed`` is handed both, and left-multiplying only worked for one.
The camera framing walks the whole mesh through the simulation every frame.
``rotation @ vertices`` raised for an (N, 3) array, which — inside the paint
handler — blanked the view the moment playback started.
"""
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
index = 40
point , normal = bundle . vertices_mm [ index ] , bundle . vertex_normals ( ) [ index ]
@@ -1618,11 +1496,9 @@ def test_the_placement_covers_a_point_and_a_whole_vertex_array(qt_app, bundle):
for index in ( 0 , 5 , 200 , bundle . vertices_mm . shape [ 0 ] - 1 ) :
single = preview . _placed ( bundle . vertices_mm [ index ] ) # noqa: SLF001
assert np . allclose ( placed [ index ] , single )
finally :
preview . deleteLater ( )
def test_the_surface_keeps_its_residual_colours_while_the_machine_moves ( qt_app , bundle ) :
def test_the_surface_keeps_its_residual_colours_while_the_machine_moves ( bundle , preview ) :
""" The shading is the measurement, so it stays on through playback.
It used to switch to a flat " moving body " grey, which threw away the one thing
@@ -1630,11 +1506,8 @@ def test_the_surface_keeps_its_residual_colours_while_the_machine_moves(qt_app,
tool axis. The body itself *does* move — from the initial state when playback
starts to the solved state it rests at — and that is checked here too.
"""
from orisys_autocal . ui . pose_preview import PosePreview
stack = PlatformStack ( )
preview = PosePreview ( )
try :
preview . set_bundle ( bundle )
index = 40
point , normal = bundle . vertices_mm [ index ] , bundle . vertex_normals ( ) [ index ]
@@ -1670,8 +1543,6 @@ def test_the_surface_keeps_its_residual_colours_while_the_machine_moves(qt_app,
from orisys_autocal . ui . pose_preview import residual_colour
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 = [ ]