portrait: a unit pane honours Model:SetPosition
`SetPosition` is the widget's model-root translate and the extract spent it only on the file-pane
path. The booth path dropped the value and posed `R(yaw) · S(s)` alone, so Tmog's wheel zoom, its
right-drag pan and the per-slot framing of every browse card moved nothing at all.
It shows on a `<PlayerModel>` and not on a plain `<Model>` for one reason: this widget's camera is
frozen (`0x7acf10`) where `<Model>`'s perspective leg republishes its authored camera through the
very root being moved (`0x718960`), which cancels. `capture::probe_model_camera`'s `position` leg
pins that half; this is the other.
The extract publishes the translate in bevy's axes into a new `PanePositions`, keyed by slot so the
four stock window panes and the stock dressing room are served by the same channel rather than a
second path; a slot that is not drawn keeps the translation it has rather than snapping back to the
origin. The booths apply it as one `RootPose { yaw, scale, pos }`, latched together — keyed on
`(yaw, scale)` alone a position-only change compares equal and writes nothing, so a wheel would
zoom once and then stop. `WOW_BOOTH_LOG` gains `pane pose slot=… yaw=… scale=… pos=[…]`, one line
per change.
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, and this booth's root carries
only the renormalizer (`framing::pane_root_scale_at` drops `modelScale` and `layoutScale` both), so
taking the translate's share alone would move a `scale="4"` pane's model four times its own size.
The magnitude is the one part nothing in the tree pins — the only two calibrated `SetPosition`
sites are the ortho leg's pixel ladder and the `<Model>` leg where it cancels — and the extract's
arm says what to revisit if a wheel notch turns out to zoom too hard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -439,14 +439,14 @@ pub(super) fn tick_script(
|
||||
// `FrameHandle` the next VM will reuse for something else.
|
||||
booths.panes.0.clear();
|
||||
booths.bake_sizes.0.clear();
|
||||
booths.bake_sizes.0.clear();
|
||||
booths.positions.0.clear();
|
||||
booths.unit_panes.release_all();
|
||||
return;
|
||||
};
|
||||
let Ok(window) = window.single() else {
|
||||
booths.panes.0.clear();
|
||||
booths.bake_sizes.0.clear();
|
||||
booths.bake_sizes.0.clear();
|
||||
booths.positions.0.clear();
|
||||
booths.unit_panes.release_all();
|
||||
return;
|
||||
};
|
||||
@@ -983,6 +983,7 @@ pub(super) fn paint_script(
|
||||
// entry simply did not change ([`crate::portrait::UnitPaneBooths::sweep_begin`]).
|
||||
booths.panes.0.clear();
|
||||
booths.bake_sizes.0.clear();
|
||||
booths.positions.0.clear();
|
||||
booths.unit_panes.sweep_begin();
|
||||
let mut spans: Vec<u32> = Vec::with_capacity(prev.extracted.len());
|
||||
for eq in extracted {
|
||||
@@ -1238,6 +1239,32 @@ fn convert_entry(
|
||||
let Some(slot) = booths.model_pane_slot(name.as_deref(), handle, unit, facing) else {
|
||||
return;
|
||||
};
|
||||
// `SetPosition`, published before the readiness check as the aspect is below: the
|
||||
// booth root's translate, in bevy's axes. The order is the widget's — `(depth,
|
||||
// screen-right, up)` in model space, the camera standing off on the model's `+x` —
|
||||
// and it is converted here so no booth has to know it, exactly as the file-pane leg
|
||||
// above hands `root_pos` to `wow_to_bevy`.
|
||||
//
|
||||
// Unlike the file pane's, this one *moves* the model: a `<PlayerModel>`'s camera is
|
||||
// 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
|
||||
// `<Model>` 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.
|
||||
booths.positions.0.insert(
|
||||
slot.to_string(),
|
||||
benilla_assets::coords::wow_to_bevy([position.0, position.1, position.2]),
|
||||
);
|
||||
// The aspect is published before the readiness check: the bake waits on the publish.
|
||||
if rect.height() > 0.0 {
|
||||
booths
|
||||
@@ -1733,6 +1760,7 @@ mod clip_plumb_tests {
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::portrait::PaneBakeSizes>();
|
||||
app.init_resource::<crate::portrait::PanePositions>();
|
||||
app.init_resource::<crate::portrait::UnitPaneBooths>();
|
||||
app.init_resource::<crate::ui_models::UiModelTiles>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
@@ -1821,6 +1849,7 @@ mod clip_plumb_tests {
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::portrait::PaneBakeSizes>();
|
||||
app.init_resource::<crate::portrait::PanePositions>();
|
||||
app.init_resource::<crate::portrait::UnitPaneBooths>();
|
||||
app.init_resource::<crate::ui_models::UiModelTiles>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
@@ -2017,6 +2046,7 @@ mod extract_gate_tests {
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::portrait::PaneBakeSizes>();
|
||||
app.init_resource::<crate::portrait::PanePositions>();
|
||||
app.init_resource::<crate::portrait::UnitPaneBooths>();
|
||||
app.init_resource::<crate::ui_models::UiModelTiles>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
@@ -2215,6 +2245,89 @@ mod extract_gate_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Model:SetPosition` is the pane's model-root translate (`T(pos · layoutScale)`,
|
||||
/// `0x76d1a0`), and on a `<PlayerModel>` it *moves the model*: the widget's camera is frozen
|
||||
/// (`0x7acf10`) rather than republished through the root, so unlike a plain `<Model>`'s
|
||||
/// perspective leg — where it provably cancels, which
|
||||
/// [`crate::capture::probe_model_camera`]'s `position` leg pins — the pane zooms and pans.
|
||||
/// The booth path used to drop the call on the floor, which cost Tmog its wheel zoom, its
|
||||
/// right-drag pan and the per-slot framing of every browse card.
|
||||
///
|
||||
/// The axes are the widget's — `SetPosition(depth, right, up)` in model units, the camera
|
||||
/// standing off on the model's `+x` — so bevy's translate is `(-right, up, -depth)` and a
|
||||
/// wheel-up (`+depth`) walks the model toward the camera.
|
||||
#[test]
|
||||
fn a_unit_pane_publishes_its_set_position_as_its_booth_roots_translate() {
|
||||
use crate::portrait::{PanePositions, UNIT_PANE_SLOTS};
|
||||
let mut app = app_from_script(
|
||||
r#"
|
||||
local doll = CreateFrame("PlayerModel", "CharacterModelFrame")
|
||||
doll:SetPoint("TOPLEFT", 0, 0)
|
||||
doll:SetWidth(233); doll:SetHeight(224)
|
||||
doll:SetUnit("player")
|
||||
doll:SetPosition(1, 0, 0)
|
||||
|
||||
local p = CreateFrame("PlayerModel", "TmogFramePlayerModel")
|
||||
p:SetPoint("TOPLEFT", 300, -40)
|
||||
p:SetWidth(300); p:SetHeight(400)
|
||||
p:SetUnit("player")
|
||||
p:SetPosition(2, 0.5, -0.25)
|
||||
"#,
|
||||
);
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.set_unit(
|
||||
"player",
|
||||
Some(benilla_ui::script::UnitState {
|
||||
guid: 0xdead_beef,
|
||||
exists: true,
|
||||
..default()
|
||||
}),
|
||||
);
|
||||
app.update();
|
||||
let at =
|
||||
|app: &App, slot: &str| app.world().resource::<PanePositions>().0.get(slot).copied();
|
||||
assert_eq!(
|
||||
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"
|
||||
);
|
||||
// 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!(
|
||||
at(&app, "paperdoll"),
|
||||
Some(Vec3::new(0.0, 0.0, -1.0)),
|
||||
"a named pane publishes under its window's booth"
|
||||
);
|
||||
// One model unit 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.
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.run("TmogFramePlayerModel:SetScale(0.5)")
|
||||
.unwrap();
|
||||
app.update();
|
||||
assert_eq!(
|
||||
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"
|
||||
);
|
||||
// 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.
|
||||
app.world_mut()
|
||||
.non_send_resource_mut::<UiScript>()
|
||||
.run("TmogFramePlayerModel:Hide()")
|
||||
.unwrap();
|
||||
app.update();
|
||||
assert_eq!(
|
||||
at(&app, UNIT_PANE_SLOTS[0]),
|
||||
None,
|
||||
"an undrawn pane publishes nothing"
|
||||
);
|
||||
}
|
||||
|
||||
/// The cap is only acceptable if a booth comes back: without a release the first eight panes
|
||||
/// to draw would own the pool for the session, and pfUI's ninth unit frame would be
|
||||
/// permanently empty. A pane hiding frees its slot, and the pane that was refused takes it —
|
||||
|
||||
@@ -463,6 +463,7 @@ mod caret_tests {
|
||||
app.init_resource::<PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::portrait::PaneBakeSizes>();
|
||||
app.init_resource::<crate::portrait::PanePositions>();
|
||||
app.init_resource::<crate::portrait::UnitPaneBooths>();
|
||||
app.init_resource::<crate::ui_models::UiModelTiles>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
|
||||
@@ -481,6 +481,7 @@ fn a_shown_ping_pane_asks_for_a_tile_and_draws_its_cell() {
|
||||
app.init_resource::<crate::portrait::PortraitImages>();
|
||||
app.init_resource::<crate::portrait::BoothPanes>();
|
||||
app.init_resource::<crate::portrait::PaneBakeSizes>();
|
||||
app.init_resource::<crate::portrait::PanePositions>();
|
||||
app.init_resource::<crate::portrait::UnitPaneBooths>();
|
||||
app.init_resource::<UiModelTiles>();
|
||||
app.init_resource::<crate::minimap::MinimapWidget>();
|
||||
|
||||
Reference in New Issue
Block a user