formats: teach the renderer to rotate (Option A) -- and report that it does not close the title
The human chose Option A: teach sylpheed-formats own renderer to draw rotation_deg so it and the port stay comparable and verify-screen keeps meaning someone is wrong. Three pieces, because rotation alone does nothing on the title. blit gains a rotated path that draws by inverse mapping over the rotated bounding box, turning about the pivot, whose absolute position is invariant under scale; zero rotation keeps the original forward-mapped path byte for byte so non-rotating screens cannot regress. compose draws a nested .rat leaf when the leaf carries geometry the parent does not, which is the sweeps case, but not as a blanket rule since a button s leaf duplicates its parent. And --at poses leaves at a keyframe time, because the sweeps hold off-screen at x=1521 so a resting composite omits them. A trap found the hard way: posing EVERYTHING at one global time is wrong, because a top-level group s final keyframes are its exit ramp and rest() deliberately stops before them. Posing the title at t=358 walked every parent into its exit and drove the disagreement from 10.92 to 61.74. So at poses leaves only. Controls: 0 and 360 degrees byte-identical to the unrotated path, 90 degrees swaps a 10x4 to 4x10, area conserved within 15 percent, centroid stays on the pivot. 116 lib tests pass, main_menu unchanged at 9.26. And the verification did not show what it was meant to, which is reported rather than buried: scanning the pose time against the title capture gives 10.73 to 11.17 against a 10.92 baseline -- flat, no minimum, best 1.7 percent. The whole-frame mean is dominated by the tone curve, and the renderer still does not draw ptlogo1/ptlogo2 at all, which is a far larger spatial gap than two translucent sweeps. So rotation is correct in isolation and no screen regressed, but whether it closes the port s 1.81 percent is not established here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QsEPXWVaEpyfudtR6re1Pd
This commit is contained in:
90
docs/re/structures/ui-rotation-implemented.md
Normal file
90
docs/re/structures/ui-rotation-implemented.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# ✅ Option A implemented — the reference renderer rotates. ⚠️ It does not close the title.
|
||||
|
||||
**Decision:** the human chose **Option A** (2026-08-29) — teach
|
||||
`sylpheed-formats`' own renderer to draw `rotation_deg`, so it and the port stay
|
||||
comparable and `verify-screen` keeps meaning *"someone is wrong"*.
|
||||
|
||||
**Status:** ✅ implemented and controlled. 🔴 **and it does not measurably improve
|
||||
the title against the capture we hold** — reported here rather than quietly, because
|
||||
the improvement was the reason for doing it.
|
||||
|
||||
## What changed
|
||||
|
||||
Three pieces, because rotation alone does nothing on the title:
|
||||
|
||||
1. **`blit` gained a rotated path.** `rotation_deg != 0` draws by **inverse
|
||||
mapping** over the rotated bounding box; forward-mapping a rotation leaves
|
||||
gaps. Rotation turns about the element's **pivot**, whose absolute position
|
||||
`(kf.x + pivot_x, kf.y + pivot_y)` is invariant under scale.
|
||||
✅ `rotation_deg == 0` keeps the original forward-mapped path **byte for
|
||||
byte**, so screens that do not rotate cannot regress.
|
||||
2. **`compose` draws a nested `.rat` leaf when the leaf carries geometry the
|
||||
parent does not** — the title's sweeps are exactly that case (parent fixed at
|
||||
(441,270) scale 100 %, leaf holding 600 %/800 % and ±30°/−45°).
|
||||
⚠️ Not a blanket rule: a button's leaf *duplicates* its parent and the parent
|
||||
wins ([`ui-leaf-vs-parent-alpha.md`](ui-leaf-vs-parent-alpha.md)), so the leaf
|
||||
is used only when its pose genuinely differs.
|
||||
⚠️ A leaf element resolves **no sprite of its own** — names resolve against
|
||||
the bundle a build was parsed from, and a leaf is parsed from its own slice.
|
||||
Its *name* is the sprite name, looked up in the parent bundle's table.
|
||||
3. **`--at <units>` / `ComposeOptions::at`**, because the sweeps hold off-screen
|
||||
at `x = 1521` and a resting composite therefore *omits* them.
|
||||
|
||||
## 🔴 A trap this found, and it cost a wrong number first
|
||||
|
||||
Posing **everything** at one global time is wrong. A top-level group's final
|
||||
keyframes are its **exit ramp** — the fade-out played when the screen leaves —
|
||||
and `rest()` deliberately stops at the last *hold* keyframe before it. Posing the
|
||||
title at t=358 walked every parent into its exit and drove the disagreement from
|
||||
**10.92 to 61.74**.
|
||||
|
||||
✅ `at` therefore poses **leaves only**; top-level elements keep `rest()`. That
|
||||
follows the decoded rule directly: the leaf runs on its own timeline and the
|
||||
parent's does not gate it.
|
||||
|
||||
## The controls
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 0° and **360°** vs the unrotated path | **byte-identical** |
|
||||
| 90° on a 10×4 sprite | extents swap to **4×10** |
|
||||
| covered area under rotation | conserved to **< 15 %** |
|
||||
| centroid under rotation | stays on the pivot (< 1 px) |
|
||||
|
||||
`rotation_control_known_angles` pins all four. **116 lib tests pass.**
|
||||
|
||||
## 🔴 The verification, which did not show what it was meant to
|
||||
|
||||
Rendering the title against
|
||||
[`live-title-build4-no-plate.png`](../captures/title-builds/live-title-build4-no-plate.png)
|
||||
and scanning the pose time:
|
||||
|
||||
| | mean abs difference |
|
||||
|---|---|
|
||||
| before (rest, no leaves, no rotation) | **10.92** |
|
||||
| after, scanned t = 0 … 600 | **10.73 – 11.17** |
|
||||
| best (t = 420) | 10.73 — **1.7 %** better |
|
||||
|
||||
**Flat. No minimum.** Drawing the sweeps correctly does not measurably improve
|
||||
this comparison, and two things explain why without rescuing it:
|
||||
|
||||
* the whole-frame mean is dominated by the **tone curve**, which
|
||||
[`title-residual-tone-vs-geometry.md`](title-residual-tone-vs-geometry.md)
|
||||
measures as the larger part of the *level* difference even where geometry is
|
||||
right;
|
||||
* our renderer still **does not draw `ptlogo1` / `ptlogo2` at all** (four
|
||||
elements, reported as "not drawn"), and that is a far larger spatial gap than
|
||||
two translucent sweeps.
|
||||
|
||||
⚠️ **So the honest claim is narrow:** rotation is implemented and correct in
|
||||
isolation, and no screen regressed. Whether it closes the port's **1.81 % of
|
||||
pixels differing** is **not established here** — that harness poses deliberately
|
||||
and counts differing pixels rather than mean level, and it is the place to judge
|
||||
it. ❔ The sweeps may simply be a small term.
|
||||
|
||||
## No regression
|
||||
|
||||
| screen | before | after |
|
||||
|---|---|---|
|
||||
| `main_menu` | 9.26 | **9.26** |
|
||||
| `extras` | — | 9.75 |
|
||||
Reference in New Issue
Block a user