From 072d69144903c45c9457b98246a4aaf47bcc9975 Mon Sep 17 00:00:00 2001 From: grom Date: Fri, 16 Oct 2026 17:36:35 -0600 Subject: [PATCH] portrait: fit SetPosition's unit to the one table that calibrates it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shipping one model unit per `SetPosition` unit put Tmog's browse cards behind their own cameras and the grid drew empty: its per-slot table asks for depths of 3.8 (gnome) to 11.8 (night elf) against authored cameras that stand about 2 to 6 units off. That table is itself the measurement — each row has to land that race's head in a card — and across gnome, dwarf, human and night elf the displacement they need sits at 0.45–0.55 of what they ask for. `G48`, the client's own `1/√(a²+1)` at `[0x832a48]`, is 0.50 on a 16:9-ish panel and 0.6 at 4:3, and it is the constant `Model::layout_unit` already inverts for the orthographic leg. So the translate is `pos · G48`. Fitted, not traced, and the extract's arm says so: a capture of the reference's root would settle it, and the wheel zoom this landed for in the first place works at either value because the player drives it by hand. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- .../benilla-app/src/ui_script/extract/mod.rs | 70 +++++++++++++------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/crates/benilla-app/src/ui_script/extract/mod.rs b/crates/benilla-app/src/ui_script/extract/mod.rs index 3e09cf1a..adb75b60 100644 --- a/crates/benilla-app/src/ui_script/extract/mod.rs +++ b/crates/benilla-app/src/ui_script/extract/mod.rs @@ -1249,21 +1249,36 @@ fn convert_entry( // frozen (`0x7acf10`) rather than republished through the root, so the pane zooms and // pans — Tmog's wheel and right-drag. // - // **One model unit per `SetPosition` unit, and deliberately not the reference's - // `pos · layoutScale`.** That factor belongs to the group the reference's root scales - // with it (`T(pos·layout) · R(facing) · S(G48·(5/3)·modelScale·layout)`), and this - // booth's root carries only the renormalizer — `framing::pane_root_scale_at` drops - // both `modelScale` and `layoutScale` — so taking the translate's share alone would - // move a `scale="4"` pane's model four times its own size. Tmog's panes are - // `scale="4"` cards and a `scale="2"` preview (`Tmog.xml:148,579`), so that is not a - // hypothetical. The magnitude is the one part of this nothing in the tree pins: the - // only two calibrated `SetPosition` sites are the ortho leg's pixel ladder and the - // `` perspective leg where it provably cancels. If a wheel notch in Tmog turns - // out to zoom far too hard, or a browse card frames past its model, the factor is - // what to revisit — together with the root's missing `S` terms, not separately. + // **`G48` model units per `SetPosition` unit**, `G48 = 1/√(a²+1)` for the screen + // aspect — the client's own `[0x832a48]`, the same constant `Model::layout_unit` + // inverts for the orthographic leg. Deliberately not the reference's `pos · + // layoutScale`: that factor belongs to the group the reference's root scales with it + // (`T(pos·layout) · R(facing) · S(G48·(5/3)·modelScale·layout)`) and this booth's + // root carries only the renormalizer, so spending the translate's share alone would + // move a `scale="4"` pane's model four times its own size. + // + // The constant is **fitted, not traced**, and the fit is Tmog's own shipped table: + // `Positions[slot][race][sex]` frames a browse card on the slot being browsed, so + // each row is a displacement that must land the model's head in frame against that + // race's authored camera. Across gnome (3.8, standoff ≈2.2), dwarf (6.3), human (8.8, + // standoff 4.02) and night elf (11.8, the tallest) the ratio sits at 0.45–0.55, and + // `G48` is 0.50 at a 16:9-ish panel. Shipping 1.0 put every card's model behind its + // own camera and the grid drew empty, which is what pinned the order of magnitude. + // A capture of the reference's root would settle it properly; until then this is a + // measurement of an addon, not of the client. + let diag = if h > 0.0 { + ((w / h) * (w / h) + 1.0).sqrt() + } else { + 5.0 / 3.0 + }; + let g48 = 1.0 / diag; booths.positions.0.insert( slot.to_string(), - benilla_assets::coords::wow_to_bevy([position.0, position.1, position.2]), + benilla_assets::coords::wow_to_bevy([ + position.0 * g48, + position.1 * g48, + position.2 * g48, + ]), ); // The aspect is published before the readiness check: the bake waits on the publish. if rect.height() > 0.0 { @@ -2287,19 +2302,28 @@ mod extract_gate_tests { app.update(); let at = |app: &App, slot: &str| app.world().resource::().0.get(slot).copied(); - assert_eq!( + // `G48` is irrational at most aspects, so compare to a tolerance, not a bit pattern. + let near = |got: Option, want: Vec3, what: &str| { + let got = got.unwrap_or_else(|| panic!("{what}: nothing published")); + assert!( + got.abs_diff_eq(want, 1e-5), + "{what}: got {got:?}, want {want:?}" + ); + }; + // The window is 1024x768 in this harness, so `G48 = 1/√((4/3)²+1) = 0.6`. + near( at(&app, UNIT_PANE_SLOTS[0]), - Some(Vec3::new(-0.5, -0.25, -2.0)), - "an addon's own pane publishes under the pooled slot it claimed" + Vec3::new(-0.3, -0.15, -1.2), + "an addon's own pane publishes under the pooled slot it claimed", ); // A stock name takes its window's booth, and its pane's position with it: the channel is // keyed by slot precisely so the four window panes are not a second path. - assert_eq!( + near( at(&app, "paperdoll"), - Some(Vec3::new(0.0, 0.0, -1.0)), - "a named pane publishes under its window's booth" + Vec3::new(0.0, 0.0, -0.6), + "a named pane publishes under its window's booth", ); - // One model unit per `SetPosition` unit: the pane's own layout scale does **not** + // `G48` model units per `SetPosition` unit: the pane's own layout scale does **not** // multiply it, because this booth's root does not scale by it either // (`framing::pane_root_scale_at`). Taking the translate's share of the reference's // `T(pos·layout) · R · S(…·layout)` group while dropping the scale's would move a @@ -2309,10 +2333,10 @@ mod extract_gate_tests { .run("TmogFramePlayerModel:SetScale(0.5)") .unwrap(); app.update(); - assert_eq!( + near( at(&app, UNIT_PANE_SLOTS[0]), - Some(Vec3::new(-0.5, -0.25, -2.0)), - "a scaled pane moves its model by the same model units" + Vec3::new(-0.3, -0.15, -1.2), + "a scaled pane moves its model by the same model units", ); // A pane that stops drawing publishes nothing at all, so the booth keeps where it was // put ([`crate::portrait::pane_position`]) instead of reading an absent entry as 0.