added motion trajectory simulation
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
# The finger screen's 3D view
|
||||
|
||||
What the picture draws, what each colour means, and which of those meanings is a
|
||||
measurement rather than a convenience. Written because the view had grown two
|
||||
coordinate systems, a path, three markers and four competing hues, and the question
|
||||
"which line is which?" was being answered from memory.
|
||||
|
||||
## What is in the scene
|
||||
|
||||
| Element | Colour | What it is |
|
||||
| --- | --- | --- |
|
||||
| Sensing surface | green → red, shaded per vertex | **The measurement.** Green is an aligned normal, red is `BAD_RESIDUAL_DEG` (20°) or worse of residual. The shading stays on during playback. |
|
||||
| Shell | dark grey wireframe | The housing mesh from the export, drawn so the face has a body to belong to. |
|
||||
| Origin plane | grey grid, translucent | The platform's `z = 0` plane, i.e. the plane the two rotation axes lie in. **This is the initial state and it never moves.** |
|
||||
| Table marker | violet ring + cross-hairs | The table's *own* frame as it now stands: the stages' rotation and the table's XY applied to a circle drawn in the origin plane. While a move has not finished, a fainter ring shows where it ends up. Square grid = before, violet ring = now, faint ring = after. |
|
||||
| Table triad | red / green / blue, full strength | The machine's X/Y/Z, drawn at the **table origin**. The only full-strength triad in the picture, so "the machine" is readable at a glance. |
|
||||
| Rotation axes | grey, long | The yaw and pitch axes. They must cross at the one origin; a misalignment shows as the two lines missing each other instead of hiding inside the body. |
|
||||
| Origin | orange dot | The point the stages turn and the tables translate *reference to*. |
|
||||
| Base plane | amber outline | The sensor's mounting face: part of the **body**, so it turns with the stages and rides the table. Its height in the initial state is the sensor's z setup offset — the mounting plate. |
|
||||
| Tool axis | yellow line, arrowhead | The `+Z` direction the sensing face has to meet, drawn at the tool's own position in the table frame. **It does not follow the contact**: the tool is bolted to the machine and the table brings the point to it. |
|
||||
| Planned path | cyan line with arrowheads, cyan end dot | The contact's route: the stage rotation arc about the table origin, then the straight table move. |
|
||||
| Picked contact | white dot | The point that was picked on the UV map. It travels onto the tool during playback. |
|
||||
| Contact normal | blue line, arrowhead | The picked vertex's normal. It is what the solved rotation aims at the tool axis. |
|
||||
| Selected point | red dot | The point selected in the table below — the one a run would start from. |
|
||||
|
||||
## Setup: where the tool and the sensor actually are
|
||||
|
||||
Three numbers that are the same on the drawing and never the same on the bench. All
|
||||
three default to zero, and all three have to be measured on the real machine:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| **Tool X / Y** | The tool's position in the table frame. Every XY move exists to bring the contact here, so this is what the reported move is measured against. It used to be taken as the patch's centre — a stand-in that made the picture self-consistent and the numbers meaningless. **A tool-tip offset is this field**, not a second one. It *was* a second one, added to the reported coordinates while the drawing left the geometry alone: a machine with a 10 mm tip offset reported one landing point and drew another, 10 mm away. A stored `finger_tool_offset_mm` is folded in here when preferences load. |
|
||||
| **Sensor X / Y** | How far the mounted sensor is from the table origin, along the table's own axes. |
|
||||
| **Sensor Z / yaw** | The mounting height — **this is the mounting plate**, shims included, so there is no separate plate field — and the sensor's misalignment with the table about its z. |
|
||||
|
||||
### What the screen shows at rest, and what playing it does
|
||||
|
||||
The screen is a solve view, so **at rest everything stands at the end of the move**:
|
||||
the body, the contact marker, the tool's position and the table marker. The fixed
|
||||
grid is the frame they started in and the cyan path is drawn from where the contact
|
||||
was, so "will it reach, and where does the table finish" is answerable without
|
||||
watching anything. Pressing play runs the move from the initial state to that same
|
||||
end state — which is what makes the path checkable rather than decorative.
|
||||
|
||||
The one line at the bottom of the rail is the x-y check, printed as two pairs:
|
||||
|
||||
```
|
||||
落点 X/Y +10.000 +0.000 mm / 刀具 X/Y +10.000 +0.000 mm ✓ 已对准
|
||||
```
|
||||
|
||||
They are computed two different ways on purpose — the landing by pushing the contact
|
||||
through the *drawing's* transform, the tool from its setup number — so "x and y are
|
||||
aligned" is a measurement rather than a judgement about a picture. Note that the
|
||||
reported move is **not** simply `tool − platform position`: the stages turn the point
|
||||
about the origin first, so the move is `tool_xy − (R_pitch · R_yaw · p_table).xy`.
|
||||
When the two do not match, the line says so, with the distance.
|
||||
|
||||
A point of view worth knowing: at rest the sensing face is aimed along the tool axis,
|
||||
which can put the face on the far side of the body from the default camera. The tool
|
||||
axis is drawn with depth testing **off** for that reason — it is a reference line,
|
||||
and hiding it behind the body it is meant to align with is the opposite of useful.
|
||||
|
||||
The chain the solver and the drawing share is, in order: **mount** (sensor axes into
|
||||
table axes) → **setup** (the mounting pose above) → **stages** (the solved yaw and
|
||||
pitch, about the table origin) → **table** (the XY slide). A point of the sensor is
|
||||
therefore at
|
||||
|
||||
```
|
||||
p_table = R_z(sensor_yaw) · (mount · p_sensor) + sensor_offset
|
||||
p_final = R_pitch · R_yaw · p_table + (table_x, table_y, 0)
|
||||
```
|
||||
|
||||
and the solver reports the move that puts the contact at the tool:
|
||||
|
||||
```
|
||||
(table_x, table_y) = tool_xy − (R_pitch · R_yaw · p_table).xy
|
||||
```
|
||||
|
||||
Two consequences worth knowing, both covered by tests:
|
||||
|
||||
* the mounting yaw is **absorbed by the stage** — a sensor mounted 10° out needs the
|
||||
yaw stage to turn 10° less, which is why the field exists;
|
||||
* the mounting z is a lift along the table's z, which the mounting yaw leaves alone,
|
||||
which is why the plate can live in that same field instead of being a second
|
||||
number that drifts away from it.
|
||||
|
||||
`Pose.target_xy_mm` says where the contact *is*; `Pose.table_xy_mm` says what the
|
||||
table must *do* about it. The panel shows both, and the trajectory summary is the
|
||||
same claim again: the contact's last drawn position **is** the tool, to within
|
||||
1e-6 mm (`test_the_path_ends_where_the_pose_says_the_table_moves_to`).
|
||||
|
||||
## The path, and where it starts
|
||||
|
||||
`trajectory()` returns the contact's route in the order the machine moves it:
|
||||
|
||||
1. **rotation** — the stages turn the mounted body about the table origin, sweeping
|
||||
the contact along an arc whose radius is the contact's distance from that origin
|
||||
(reported as `arc_radius_mm`);
|
||||
2. **translation** — the table slides the contact onto the tool axis, in the table's
|
||||
own XY, in a straight line (reported as `reach_mm`, the total distance travelled).
|
||||
|
||||
Nothing in the path is *at* the table origin. The origin is what the stages turn
|
||||
about; the path is traced by a point on the body, so it starts at the picked contact
|
||||
and ends under the tool. The two numbers a reader needs in order to judge the path —
|
||||
how far the contact travels, and how large the swing is — are printed beside the
|
||||
readouts.
|
||||
|
||||
The palette lives in one place, `pose_preview.COLOUR`, with a rule the view used to
|
||||
break: **a colour identifies exactly one thing**. Two things sharing a hue read as a
|
||||
duplicate or a contradiction, which is what "messy" turned out to mean. An import
|
||||
check (`_check_palette_has_no_collisions`) refuses a duplicate, and
|
||||
`set_colour`/`set_rgb` accept either 3- or 4-channel entries so the length of a
|
||||
palette tuple can never abort a paint again — it did once, and silently deleted the
|
||||
base plane, the contact and the whole trajectory from the view.
|
||||
|
||||
## Two ways the picture can lie
|
||||
|
||||
Both were real, and both looked like a plausible picture rather than a fault:
|
||||
|
||||
* **A half-drawn scene.** A `TypeError` from a colour call inside `paintGL` abandons
|
||||
every draw call after it. The remaining picture still looks like a picture, so the
|
||||
failure is invisible until someone counts what should be there. `preview.problem`
|
||||
now reaches the panel through `problemChanged`, and is drawn on the widget.
|
||||
* **A camera that frames the wrong thing.** The eye distance was
|
||||
`half the mesh's bounding-box diagonal × 3`, which cropped the table plane and the
|
||||
far end of the trajectory exactly when a pose swung them outwards. Framing now
|
||||
solves the distance from the field of view and the bounding sphere of everything
|
||||
drawn (`scene_points`, `scene_radius`, `_frame_distance`), so the whole scene fits
|
||||
at the default zoom at any pane shape. Long reference lines are excluded on
|
||||
purpose: they are meant to run off the edge, and including them would shrink the
|
||||
sensor to a speck.
|
||||
* **A render and a projection that disagreed.** Two separate faults, both invisible
|
||||
on their own and both found by checking a projected landmark against the pixels:
|
||||
`project()` left out the frustum's near-plane factor (`x_ndc = (near/right)·x/depth`),
|
||||
scaling every projected point by about 100; and `_draw` aimed the camera at the
|
||||
*raw* mesh centre while `_camera`/`project` orbited the *placed* one, so the drawn
|
||||
scene and the projected overlay sat in different places as soon as the body was
|
||||
drawn anywhere but at the identity. The axis labels were the visible casualty.
|
||||
`preview_snapshot.py --debug` now reports, for every shot, how many pixels of a
|
||||
known colour are near the projection of a known point — origin, tool line, table
|
||||
marker — so a drift shows up as a number instead of a shrug.
|
||||
|
||||
## The path, and where it starts
|
||||
|
||||
`trajectory()` returns the contact's route in the order the machine moves it:
|
||||
|
||||
1. **rotation** — the stages turn the body about the table origin, sweeping the
|
||||
contact along an arc whose radius is the contact's distance from that origin
|
||||
(reported as `arc_radius_mm`);
|
||||
2. **translation** — the table slides the contact onto the tool axis in a straight
|
||||
line (reported as `reach_mm`, the total distance travelled).
|
||||
|
||||
Nothing in the path is *at* the table origin. The origin is what the stages turn
|
||||
about; the path is traced by a point on the body, so it starts at the picked contact
|
||||
and ends under the tool. The two numbers a reader needs in order to judge the path —
|
||||
how far the contact travels, and how large the swing is — are printed next to the
|
||||
readouts.
|
||||
|
||||
The drawn path and the animation are the same transform by construction:
|
||||
`trajectory()` samples `body_transform()`, the function the drawing uses. They did
|
||||
not always: the path was sampled up to `SIM_ROTATION_SHARE` of the solved rotation —
|
||||
the *time* that phase takes in the animation, not how far it turns — so the drawn
|
||||
arc ended 45% short and the straight move began from a point the machine never
|
||||
passes through. `test_the_drawn_path_ends_where_the_animation_puts_the_contact` pins
|
||||
them together, and `test_the_path_ends_where_the_pose_says_the_table_moves_to` pins
|
||||
both to the reported move.
|
||||
|
||||
## Frames, in one line
|
||||
|
||||
The mesh is stored in sensor coordinates and drawn through `body_transform`; the
|
||||
machine's own frame is drawn separately, in mesh coordinates, by `table_frame()`.
|
||||
Every frame conversion goes through `PlatformStack` — `mount`, `setup_rotation`,
|
||||
`setup_point`, `platform_rotation`, `stage_rotation` — and never through a second
|
||||
copy composed in the view. Two copies of a frame convention is what produced two
|
||||
wrong orientations in opposite directions, and it is the mistake this screen was
|
||||
built to stop making.
|
||||
|
||||
## Reproducing a picture
|
||||
|
||||
The GL pane needs a real context, so the offscreen platform cannot photograph it:
|
||||
`tools/panel_snapshot.py` renders the window but leaves the 3D pane empty.
|
||||
|
||||
```powershell
|
||||
# the pane itself, large enough to judge
|
||||
python apps/autocal/tools/preview_snapshot.py --bundle <bundle> --tab sequence `
|
||||
--pane 1000x760 --points 3 --out pane.png
|
||||
|
||||
# the same, caught mid-playback
|
||||
python apps/autocal/tools/preview_snapshot.py --bundle <bundle> --tab sequence `
|
||||
--pane 1000x760 --points 1 --play 0.5 --out playing.png
|
||||
|
||||
# with a real setup: sensor x,y,z,yaw and the tool's x,y
|
||||
python apps/autocal/tools/preview_snapshot.py --bundle <bundle> --tab sequence `
|
||||
--pane 1000x760 --points 1 --setup "6,-3,5,12" --tool "7,4" --play 1.0 `
|
||||
--out tuned.png
|
||||
|
||||
# the whole page, with the pane composited in
|
||||
python apps/autocal/tools/preview_snapshot.py --bundle <bundle> --tab finger `
|
||||
--size 1920x1080 --points 3 --whole --out page.png
|
||||
```
|
||||
|
||||
`--debug` prints the state and, most usefully, where the claims land on screen: the
|
||||
path's end and the tool must project to the same pixel, and it prints the distance
|
||||
between them in mm so "they look close" is never the evidence.
|
||||
|
||||
Two things that tool has to do and are easy to get wrong: the pane is **hidden** for
|
||||
the read-back (a visible `QOpenGLWidget` is painted by the window system, which a
|
||||
session with no interactive desktop never does, so its framebuffer stays blank), and
|
||||
it retries until the framebuffer is not one flat colour (`grabFramebuffer` returns a
|
||||
valid, blank image before the first paint).
|
||||
Reference in New Issue
Block a user