portrait: measure SetPosition's unit instead of fitting it

One `WOW_BOOTH_LOG` run with Tmog open printed every term, so the factor is arithmetic now rather
than a fit:

- The cards draw through the paper-doll camera, logged `eye=(0.003, 1.103, -2.919)`, and the same
  run logged the dwarf's `head_y=1.364`.
- Tmog's browse cards frame on the slot being browsed, so its per-race **head** row is a request
  to put that race's head on the camera's axis. The dwarf row's vertical is `-1.2`.
- Centring the head needs `1.364 - 1.103 = 0.261` model units against a request of `1.2`:
  **0.2175**, rounded to 0.22.

The same run confirmed the whole chain end to end: the logged pose inverted back to
`(3.72, 0, 0.489)` at facing 3.2, and `Tmog.lua:332` reads `{ 3.8, 0, 0.5, 3.2 }` — the small gap
is the previous factor, `G48` at grom's actual aspect.

It cross-checks on depth: the head row's `6.3` then moves the model 1.37 of its 2.92 standoff,
leaving it 1.55 out — a view about 0.75 units tall around a head of roughly 0.35. The two earlier
values are what make the arithmetic worth trusting: 1.0 put every card *behind* its camera and the
grid drew empty; `G48` ≈ 0.49 left them 1.06 out, the extreme close-up in the screenshot.

Still a measurement of an addon's table against this client rather than of the reference's own
root, and the constant says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-16 19:11:13 -06:00
co-authored by Claude Opus 5
parent 3d4c242c09
commit 07d41dc321
2 changed files with 42 additions and 36 deletions
+27 -4
View File
@@ -761,10 +761,33 @@ pub(crate) fn pane_bake_size(longest_px: u32) -> u32 {
#[derive(Resource, Default)]
pub(crate) struct PaneBakeSizes(pub(crate) HashMap<String, u32>);
/// Each drawn pane's `Model:SetPosition`, as the translation its booth's model root carries: one
/// model unit per `SetPosition` unit, in bevy's axes, converted at the publish so no booth has to
/// know the widget's coordinate order. The extract's `ModelPane` arm says why the reference's
/// `· layoutScale` is deliberately left off here.
/// Model units per `SetPosition` unit on the booth path, **measured** rather than traced.
///
/// The measurement, off one `WOW_BOOTH_LOG` run with Tmog open, because every term in it is a
/// number this client printed:
///
/// - Tmog's browse cards frame on the slot being browsed, so its per-race head row is a request
/// to put that race's head on the pane camera's axis. For a dwarf that row's vertical is
/// `-1.2` (`Tmog.lua:248`).
/// - The cards draw through the paper-doll camera, logged as `eye=(0.003, 1.103, -2.919)`, and
/// the same run logged the subject's `head_y=1.364`.
/// - So the displacement that centres the head is `1.364 - 1.103 = 0.261` model units, against a
/// request of `1.2`: **0.2175**, rounded here.
///
/// It cross-checks on depth. At this factor the head row's `6.3` moves the model 1.37 of its
/// 2.92 standoff, leaving it 1.55 out — a view about 0.75 units tall around a head of roughly
/// 0.35, which is a head filling half a card. The two previous values are what make the
/// arithmetic worth trusting over a guess: 1.0 put every card *behind* its camera (the grid drew
/// empty) and `G48` ≈ 0.49 left them at 1.06, the extreme close-up grom photographed.
///
/// Still a measurement of an addon's table against this client, not of the reference's own root;
/// a capture of `0x76d1a0` would supersede it. The extract's `ModelPane` arm says why the
/// reference's `· layoutScale` is left off entirely.
pub(crate) const SET_POSITION_UNIT: f32 = 0.22;
/// Each drawn pane's `Model:SetPosition`, as the translation its booth's model root carries:
/// [`SET_POSITION_UNIT`] model units per unit, in bevy's axes, converted at the publish so no
/// booth has to know the widget's coordinate order.
///
/// A channel of its own, keyed by slot, for the reason [`PaneBakeSizes`] gives and one more: it
/// serves the four stock window panes as well as the pool, and only the extract can see a pane's
+15 -32
View File
@@ -1249,35 +1249,18 @@ fn convert_entry(
// frozen (`0x7acf10`) rather than republished through the root, so the pane zooms and
// pans — Tmog's wheel and right-drag.
//
// **`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;
// **[`SET_POSITION_UNIT`] model units per `SetPosition` unit.** 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.
booths.positions.0.insert(
slot.to_string(),
benilla_assets::coords::wow_to_bevy([
position.0 * g48,
position.1 * g48,
position.2 * g48,
position.0 * crate::portrait::SET_POSITION_UNIT,
position.1 * crate::portrait::SET_POSITION_UNIT,
position.2 * crate::portrait::SET_POSITION_UNIT,
]),
);
// The aspect is published before the readiness check: the bake waits on the publish.
@@ -2310,21 +2293,21 @@ mod extract_gate_tests {
"{what}: got {got:?}, want {want:?}"
);
};
// The window is 1024x768 in this harness, so `G48 = 1/√((4/3)²+1) = 0.6`.
// `SET_POSITION_UNIT` is 0.22: `(2, 0.5, -0.25)` lands as `(-0.11, -0.055, -0.44)`.
near(
at(&app, UNIT_PANE_SLOTS[0]),
Vec3::new(-0.3, -0.15, -1.2),
Vec3::new(-0.11, -0.055, -0.44),
"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.
near(
at(&app, "paperdoll"),
Vec3::new(0.0, 0.0, -0.6),
Vec3::new(0.0, 0.0, -0.22),
"a named pane publishes under its window's booth",
);
// `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
// A fixed number of 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
// `scale="4"` Tmog card's model four times its own size.
@@ -2335,7 +2318,7 @@ mod extract_gate_tests {
app.update();
near(
at(&app, UNIT_PANE_SLOTS[0]),
Vec3::new(-0.3, -0.15, -1.2),
Vec3::new(-0.11, -0.055, -0.44),
"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