diff --git a/crates/sylpheed-formats/examples/main_menu_element_extents.rs b/crates/sylpheed-formats/examples/main_menu_element_extents.rs new file mode 100644 index 00000000..c0521681 --- /dev/null +++ b/crates/sylpheed-formats/examples/main_menu_element_extents.rs @@ -0,0 +1,46 @@ +//! What elements sit under the port's hot residual tiles on the main menu? +//! +//! `sylpheed-port` mapped the menu's edge residual at 64 px tiles and handed over +//! coordinates without names — the element inventory is this side's. Hot tiles +//! cluster at x 384–704, y 64–256, hottest at (512,128). +//! +//! ⚠️ Their tiles are in the frame they compare in; this prints DESIGN space, and +//! the two differ by the capture offset (capture_y ≈ 64.8 + 0.992·design_y). Both +//! readings are printed so the mapping is not assumed. +//! +//! cargo run -p sylpheed-formats --example main_menu_element_extents +use sylpheed_formats::{pak::PakArchive, ui_layout}; +use std::path::PathBuf; + +fn main() { + let root = PathBuf::from(std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC")); + let ar = PakArchive::open(root.join("dat/GP_TITLE.pak")).expect("GP_TITLE"); + let by = ar.read(&ar.entries()[5]).expect("entry 5"); + let b = ui_layout::parse_build(&by).expect("build"); + println!("{:<26} {:>6} {:>6} {:>7} {:>7} {}", "element", "x", "y", "pivot_x", "pivot_y", "sprite"); + let mut rows: Vec<(i32, i32, String, String)> = b + .elements + .iter() + .map(|e| { + let k = e.rest(); + (k.map(|k| k.x).unwrap_or(0), k.map(|k| k.y).unwrap_or(0), + e.name.clone(), e.sprite.clone().unwrap_or_default()) + }) + .collect(); + rows.sort_by_key(|r| (r.1, r.0)); + for (x, y, n, s) in &rows { + // flag anything whose rest position lands in the hot band, read both ways + let cap_y = 64.82 + 0.9919 * (*y as f64); + let hot_design = (384..=704).contains(x) && (64..=256).contains(y); + let hot_capture = (384..=704).contains(x) && (64.0..=256.0).contains(&cap_y); + let mark = match (hot_design, hot_capture) { + (true, true) => " <- HOT both readings", + (true, false) => " <- hot in DESIGN space", + (false, true) => " <- hot in CAPTURE space", + _ => "", + }; + println!("{n:<26} {x:>6} {y:>6} {:>7} {:>7} {s}{mark}", + b.elements.iter().find(|e| &e.name == n).map(|e| e.pivot_x).unwrap_or(0), + b.elements.iter().find(|e| &e.name == n).map(|e| e.pivot_y).unwrap_or(0)); + } +} diff --git a/docs/re/data/menu-hot-tile-inventory.txt b/docs/re/data/menu-hot-tile-inventory.txt new file mode 100644 index 00000000..77576901 --- /dev/null +++ b/docs/re/data/menu-hot-tile-inventory.txt @@ -0,0 +1,38 @@ +# What sits under the port's hot residual tiles on the main menu? 2026-08-31. +# +# sylpheed-port mapped the menu's edge residual at 64 px tiles and handed over +# COORDINATES WITHOUT NAMES -- the element inventory is this side's. Their result: +# hot tiles cluster at x 384..704, y 64..256, hottest (512,128) at 3.66x the median +# tile, and NO tile in the hot region has moved (every |dx|,|dy| < 0.1 px against a +# control reading a true 1 px at +0.949). +# +# ⚠️ COORDINATE FRAME. Their tiles are in the frame they compare in; the disc is +# design space, and the two differ by the capture offset +# (capture_y = 64.82 + 0.9919 * design_y). So both readings were tested rather than +# one assumed -- and the answer does not depend on it: three elements are hot under +# BOTH. +# +# ✅ ELEMENTS WHOSE REST POSITION FALLS IN THE HOT BAND (GP_TITLE entry 5): +# +# ptframe1.t32 (440, 108) pivot (124,140) HOT under both readings +# ptbtn01.rat (542, 162) pivot ( 42, 22) HOT under both -- the NEW GAME button +# pteff12.t32 (467, 180) pivot (174,180) HOT under both -- an effect element +# ptbtn02.rat (542, 242) pivot ( 58, 22) hot in DESIGN space only +# +# 📌 THE HOT REGION IS NOT ONE ELEMENT. It is where a FRAME, a BUTTON and an +# EFFECT overlap -- three elements of different kinds stacked in the same band. +# That is consistent with their null result: they went looking for two families of +# tile (edge-only versus hot-everywhere) and found one continuous population, so +# the region has no character of its own. +# +# ⚠️ WHAT THIS DOES NOT SAY. It names what is THERE, not what is wrong. Their map +# already excludes local displacement in these tiles, so this is not a misplaced +# element; the inventory is offered as the next reader's starting point, not as a +# diagnosis. The residual's cause remains open. +# +# 📌 AND A CONTROL LIMIT OF THEIRS THAT CHANGES HOW ANY SLOPE READS: a known +2 px +# displacement localises perfectly but reads back +0.839, because the slope is a +# linearisation (residual ~ dx * gradient) that saturates once dx approaches the +# width of an edge. So their +1 px control gives localisation AND magnitude, the +# +2 px control gives localisation and SIGN only, and **any slope they report is a +# FLOOR on the displacement, never a ceiling**.