re(ui): the focus ring's position is decoded -- a .rat leaf parses as a build

The port needed ptbtneff01.t32's placement and was about to author it from an
eyeballed PNG measurement. It does not have to: a `.rat` leaf needs no new
reader. Its first 32 bytes have a bundle header's shape -- "RATC", 0x3c
declaration-entry size at +4, element count at +20, design 1280x720 at +24/+28 --
so ui_layout::parse_build reads it unchanged.

The control is the base record, whose position is known independently: the parent
screen reports ptbtn01.rat resting at (542,162), and parsing the leaf alone
returns ptbtn01.t32 at (542,162). It reproduces all five buttons.

Positions are absolute design-space top-left. The ring rests at (500, 156/236/
316/396/476) for buttons 1-5 -- a uniform (-42,-6) from each button's own rest,
identical in the Japanese bundle. The bright label is a uniform (-7,-7).

Two things recorded rather than smoothed over: a leaf's placement DUPLICATES the
parent's rather than being relative to it, and the two copies are not always
byte-equal (ptbtn04's parent says y=401, its leaf says 402) -- the parent is what
compose honours, so the leaf is the source only for elements the parent does not
declare, which is exactly the ring. And `screen render --focus` is blind to the
ring for the same reason the port's exporter was: el.focused is name-based on
top-level elements and neither walks into the leaf.
This commit is contained in:
Sylpheed RE agent
2026-08-29 08:36:53 +00:00
parent 0fd8e6953e
commit 0ee0bb8565
2 changed files with 110 additions and 6 deletions

View File

@@ -0,0 +1,64 @@
//! Does a `.rat` leaf record decode with the same reader as a whole bundle?
//!
//! The port needs the position of `ptbtneff01.t32`, the focus ring, which is
//! declared *inside* the nested `ptbtn0Nf.rat` leaf and is therefore invisible
//! to anything that walks only a bundle's top-level elements.
//!
//! The leaf's first 32 bytes have the same shape as a bundle header --
//! `"RATC"`, `0x3c` declaration-entry size at `+4`, element count at `+20`,
//! design size at `+24`/`+28` -- so the hypothesis is that `parse_build` reads
//! it unchanged. The control is the BASE record `ptbtn0N.rat`, whose single
//! element's position is already known independently: the parent screen's
//! `screen info` reports `ptbtn01.rat` resting at (542,162).
//!
//! cargo run -p sylpheed-formats --example rat_leaf_placement -- <GP_TITLE.pak>
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let path = std::env::args().nth(1).expect("usage: … <pak>");
let ar = pak::PakArchive::open(&path).expect("open pak");
let entries: Vec<_> = ar.entries().to_vec();
for (ei, e) in entries.iter().enumerate() {
let Ok(bytes) = ar.read(e) else { continue };
let Some(kids) = ratc::parse(&bytes) else { continue };
// Only the title-family bundles carry ptbtn records.
if !kids.iter().any(|c| c.name == "ptbtn01f.rat") {
continue;
}
println!("=== pak entry {ei}");
for c in &kids {
if c.kind != "RATC" || !c.name.starts_with("ptbtn") {
continue;
}
let leaf = &bytes[c.offset..c.offset + c.size];
match ui_layout::parse_build(leaf) {
None => println!(" {:16} …parse_build says no", c.name),
Some(b) => {
println!(
" {:16} {}x{} {} element(s), fallback={}",
c.name,
b.design_w,
b.design_h,
b.elements.len(),
b.from_fallback
);
for el in &b.elements {
let r = el.rest();
println!(
" [{}] {:18} pivot ({:4},{:4}) rest ({:5},{:5}) kf {}",
el.index,
el.name,
el.pivot_x,
el.pivot_y,
r.map(|k| k.x).unwrap_or(-1),
r.map(|k| k.y).unwrap_or(-1),
el.keyframes.len()
);
}
}
}
}
println!();
}
}

View File

@@ -98,13 +98,53 @@ corpus can resolve. **Either choice is defensible; neither is measurable.**
Replacing is the cheaper one and is what the file's structure suggests, since
`ptbtn0Nf.t32` is a whole label rather than an overlay.
## ❔ Not established
## ✅ Where the ring is placed — DECODED 2026-08-29, no authoring needed
* **Where `ptbtneff01.t32` is placed.** The record declares two elements; the
per-element placement inside a `.rat` leaf was not decoded here. The capture
shows the ring **left of** the label, roughly on the underline's left end.
A port should take the offset from the record, and until it is decoded, from
the capture.
This page previously said the per-element placement inside a `.rat` leaf was not
decoded and that a consumer should eyeball it off a capture. **That was wrong by
omission**: a leaf needs no new reader. Its first 32 bytes have the same shape as
a bundle header — `"RATC"`, `0x3c` declaration-entry size at `+4`, element count
at `+20`, design size `1280x720` at `+24`/`+28` — so `ui_layout::parse_build`
reads it **unchanged**.
**The control is the base record**, whose position is known independently: the
parent screen reports `ptbtn01.rat` resting at `(542,162)`, and parsing the leaf
on its own returns `ptbtn01.t32` at `(542,162)`. It reproduces all five.
Positions are **absolute design-space top-left**, not offsets
([`examples/rat_leaf_placement.rs`](../../../crates/sylpheed-formats/examples/rat_leaf_placement.rs)):
| button | base | ring `ptbtneff01.t32` | Δ | label `ptbtn0Nf.t32` | Δ |
|---|---|---|---|---|---|
| `ptbtn01` | (542,162) | **(500,156)** | (42,6) | (535,155) | (7,7) |
| `ptbtn02` | (542,242) | **(500,236)** | (42,6) | (535,235) | (7,7) |
| `ptbtn03` | (542,322) | **(500,316)** | (42,6) | (535,315) | (7,7) |
| `ptbtn04` | (542,402) | **(500,396)** | (42,6) | (535,395) | (7,7) |
| `ptbtn05` | (542,482) | **(500,476)** | (42,6) | (535,475) | (7,7) |
The offset is **uniform**: `(42,6)` for the ring and `(7,7)` for the label on
every button, and identical in the Japanese bundle (pak entry 8).
⚠️ **One 1-unit disagreement, and it is real.** `ptbtn04`'s parent element rests
at y **401** while its own leaf says y **402** (and in the JP bundle the leaf says
401 against a parent 401). So a leaf's placement **duplicates** the parent's
rather than being relative to it, and the two copies are not always byte-equal.
The parent's is what `compose` honours; treat the leaf's as the source only for
elements the parent does not declare — which is exactly the ring's case.
🟡 The ring carries **2 keyframes** where the label carries 1, so it animates.
What it does between them is not decoded here.
## ⚠️ `screen render --focus` is blind to the ring, and so was this page
`el.focused` is name-based on **top-level** elements, and a screen's buttons are
`.rat` records whose focused twin is not itself a top-level element — so
rendering build 5 with and without `--focus` produces an identical image. The
reference renderer has the same blind spot the port reported, for the same
reason: neither walks into the leaf. Fixing it is a renderer change, not a
format question; the format is decoded above.
## ❔ Not established
* **`ptbtn02b.t32`** — a third variant, `b`, exists for button 02 only, same size
as the base. Not seen on any capture. Not chased.
* Whether a **non-title** archive uses the same two-element convention. Checked