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:
2026-10-16 18:45:25 -06:00
co-authored by Claude Opus 5
parent c1ef619295
commit 7fef604615
5 changed files with 247 additions and 34 deletions
+15 -6
View File
@@ -175,6 +175,7 @@ pub(super) fn sync_dressup_booth(
bake: Res<DressUpBake>,
mut booths: ResMut<Booths>,
panes: Res<super::BoothPanes>,
positions: Res<super::PanePositions>,
mut booth_light: ResMut<BoothLight>,
mut materials: ResMut<Assets<WowModelMaterial>>,
creatures: Option<Res<Creatures>>,
@@ -182,7 +183,7 @@ pub(super) fn sync_dressup_booth(
mut cams: Query<(&BoothCam, &mut Transform, &mut Projection)>,
mut palettes: ResMut<benilla_world::rig_palette::RigPalettes>,
mut env_cache: Local<Option<bool>>,
mut last: Local<Option<(u64, f32)>>,
mut last: Local<Option<(u64, f32, Vec3)>>,
// Whether a bake stands on the stage; `Booth::baked` keys the mirrored booths, not this one.
mut staged_flag: Local<bool>,
) {
@@ -194,9 +195,17 @@ pub(super) fn sync_dressup_booth(
};
// The pane's aspect, latched while on screen: the dressing room's is 316×351, not square.
let aspect = panes.0.get(DRESSUP_SLOT).copied().unwrap_or(booth.aspect);
let (last_rev, last_yaw) = last.unwrap_or((u64::MAX, f32::NAN));
// `Model:SetPosition` on the stock room's own pane, latched with the yaw because they are
// one root transform; absent, the last applied one, as [`super::pane_position`] keeps it.
let pos = positions
.0
.get(DRESSUP_SLOT)
.copied()
.or(last.map(|l| l.2))
.unwrap_or(Vec3::ZERO);
let (last_rev, last_yaw, last_pos) = last.unwrap_or((u64::MAX, f32::NAN, Vec3::NAN));
let rebake = last_rev != bake.revision || booth.aspect != aspect;
if !rebake && last_yaw == preview.yaw {
if !rebake && last_yaw == preview.yaw && last_pos == pos {
return;
}
booth.aspect = aspect;
@@ -217,7 +226,7 @@ pub(super) fn sync_dressup_booth(
booth.parked = false;
*staged_flag = false;
}
*last = Some((bake.revision, preview.yaw));
*last = Some((bake.revision, preview.yaw, pos));
return;
}
// Rig and framing come from the display cache the assembly gated on; if not ready,
@@ -266,9 +275,9 @@ pub(super) fn sync_dressup_booth(
}
commands
.entity(booth.root)
.insert(Transform::from_rotation(Quat::from_rotation_y(preview.yaw)));
.insert(Transform::from_translation(pos).with_rotation(Quat::from_rotation_y(preview.yaw)));
booth.wake = booth.wake.max(BOOTH_SETTLE_FRAMES);
*last = Some((bake.revision, preview.yaw));
*last = Some((bake.revision, preview.yaw, pos));
}
/// What [`stage_assembled_bake`] put on the stage, for a caller's log line.
pub(super) struct Staged {
+115 -26
View File
@@ -761,6 +761,26 @@ 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.
///
/// 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
/// effective scale. Written where the aspect is and cleared with it; an absent entry means "not
/// drawn this frame", which keeps the translation already applied rather than snapping the model
/// back to the origin ([`pane_position`]).
///
/// It shows at all because `<PlayerModel>`'s camera is frozen (`0x7acf10`). A plain `<Model>`'s
/// perspective leg publishes its authored camera through this very root (`0x718960`), so there
/// the translate cancels and moves nothing — which is what
/// [`crate::capture::probe_model_camera`]'s `position` leg pins. On a `<PlayerModel>` it is how
/// Tmog zooms its preview with the wheel, pans it with a right-drag, and frames each browse card
/// on the slot being browsed (`Tmog.lua`'s `Positions[slot][race][sex]`).
#[derive(Resource, Default)]
pub(crate) struct PanePositions(pub(crate) HashMap<String, Vec3>);
/// The dynamic unit-pane booths' slot names, one per pool slot. Fixed strings, not minted per
/// pane, so the whole String-keyed booth machinery ([`Booths`], [`BoothCam`], [`PortraitImages`],
/// [`BoothPanes`]) carries them with no second rendering path: a slot is a booth like any other,
@@ -1065,6 +1085,8 @@ pub(crate) struct BoothBridge<'w> {
pub(crate) unit_panes: ResMut<'w, UnitPaneBooths>,
/// How big a bake each drawn pooled pane wants ([`PaneBakeSizes`]).
pub(crate) bake_sizes: ResMut<'w, PaneBakeSizes>,
/// Where each drawn pane puts its model ([`PanePositions`]).
pub(crate) positions: ResMut<'w, PanePositions>,
}
impl BoothBridge<'_> {
@@ -1242,6 +1264,8 @@ fn pool_pace_phases(drawn: [bool; UNIT_PANE_SLOTS.len()]) -> ([f64; UNIT_PANE_SL
#[derive(bevy::ecs::system::SystemParam)]
pub(crate) struct BoothFraming<'w> {
pub(crate) panes: Res<'w, BoothPanes>,
/// `Model:SetPosition`, the model root's translate ([`PanePositions`]).
pub(crate) positions: Res<'w, PanePositions>,
pub(crate) gx: Res<'w, GxAspect>,
}
@@ -1285,6 +1309,7 @@ impl Plugin for PortraitPlugin {
.init_resource::<Booths>()
.init_resource::<BoothPanes>()
.init_resource::<PaneBakeSizes>()
.init_resource::<PanePositions>()
.init_resource::<UnitPaneBooths>()
.init_resource::<GxAspect>()
.init_resource::<BoothLight>()
@@ -2224,9 +2249,9 @@ fn sync_body_booths(
mut palettes: ResMut<benilla_world::rig_palette::RigPalettes>,
mut wow_mats: ResMut<Assets<WowModelMaterial>>,
mut env_cache: Local<Option<bool>>,
// The applied `(yaw, root scale)` per slot, which a resize re-latches without a re-bake;
// `None` is a booth with nothing posed in it.
mut last_pose: Local<HashMap<&'static str, Option<(f32, f32)>>>,
// The applied root pose per slot ([`RootPose`]), which a resize or a `SetPosition`
// re-latches without a re-bake; `None` is a booth with nothing posed in it.
mut last_pose: Local<HashMap<&'static str, Option<RootPose>>>,
mut cams: Query<(&BoothCam, &mut Transform, &mut Projection)>,
framing_in: BoothFraming,
anim_data: Option<Res<crate::creature_anim::AnimData>>,
@@ -2273,6 +2298,7 @@ fn sync_body_booths(
&mut cams,
anim_data.as_deref(),
&framing_in.panes,
&framing_in.positions,
);
}
// The dynamic pool: an addon's own pane, through the camera it asked for, animating at the
@@ -2322,6 +2348,7 @@ fn sync_body_booths(
&mut cams,
anim_data.as_deref(),
&framing_in.panes,
&framing_in.positions,
last_pose.entry(slot).or_default(),
) {
log_pane_source(slot, claim.map(|c| c.pane), why, &mut pane_source);
@@ -2352,6 +2379,7 @@ fn sync_body_booths(
&mut cams,
anim_data.as_deref(),
&framing_in.panes,
&framing_in.positions,
);
}
}
@@ -2385,6 +2413,69 @@ fn needs_restage(
staged != Some((pane, revision)) || had_aspect != aspect
}
/// The pose a pane's model root carries: the widget's own `T(pos) · R(facing) · S(s)`
/// (`0x76d1a0`), latched per slot so a frame that moves none of the three writes no transform and
/// wakes no camera.
///
/// One type for the three because they are one transform, and because the latch has to carry all
/// of it: keyed on `(yaw, scale)` alone, a frame that only moved the model would compare equal
/// and write nothing, so Tmog's wheel would zoom once and then stop.
#[derive(Clone, Copy, PartialEq, Debug)]
struct RootPose {
/// `SetFacing`/`SetRotation`'s radians, about the model's up axis.
yaw: f32,
/// [`framing::pane_root_scale_at`], the renormalizer — not `SetModelScale`.
scale: f32,
/// [`PanePositions`]: `SetPosition · layoutScale`, bevy's axes.
pos: Vec3,
}
/// The translation `slot`'s root should carry: this frame's publish, else the one it already has.
///
/// A pane that is not drawn publishes nothing, and that must not read as "back to the origin" —
/// it would re-pose and wake the camera on the frame a window closes, and again on the frame it
/// opens. The aspect latches into [`Booth::aspect`] for the same reason; the applied pose is this
/// one's latch, so it needs no field.
fn pane_position(positions: &PanePositions, slot: &str, last_pose: Option<RootPose>) -> Vec3 {
positions
.0
.get(slot)
.copied()
.or(last_pose.map(|p| p.pos))
.unwrap_or(Vec3::ZERO)
}
/// Put `pose` on the booth's model root, unless it already holds it; `force` writes regardless,
/// for a bake whose root was just rebuilt from nothing.
///
/// bevy composes a `Transform` as `T·R·S`, which is the reference's product order: the translate
/// is in model units and is **not** multiplied by the scale.
fn pose_booth_root(
commands: &mut Commands,
booth: &mut Booth,
slot: &str,
pose: RootPose,
last_pose: &mut Option<RootPose>,
force: bool,
) {
if !force && *last_pose == Some(pose) {
return;
}
commands.entity(booth.root).insert(
Transform::from_translation(pose.pos)
.with_rotation(Quat::from_rotation_y(pose.yaw))
.with_scale(Vec3::splat(pose.scale)),
);
*last_pose = Some(pose);
booth.wake = booth.wake.max(BOOTH_SETTLE_FRAMES);
if booth_log() {
eprintln!(
"[booth] pane pose slot={slot} yaw={:.3} scale={:.3} pos=[{:.3}, {:.3}, {:.3}]",
pose.yaw, pose.scale, pose.pos.x, pose.pos.y, pose.pos.z
);
}
}
/// Stage a pooled pane's own assembled look into its booth, framed and turned like the mirrored
/// path frames and turns one. `true` when the booth now holds this bake — including when it
/// already did and there was nothing to do.
@@ -2408,7 +2499,8 @@ fn stage_pane_look(
cams: &mut Query<(&BoothCam, &mut Transform, &mut Projection)>,
anim_data: Option<&crate::creature_anim::AnimData>,
panes: &BoothPanes,
last_pose: &mut Option<(f32, f32)>,
positions: &PanePositions,
last_pose: &mut Option<RootPose>,
) -> bool {
let Some(creatures) = creatures else {
return false;
@@ -2465,17 +2557,14 @@ fn stage_pane_look(
slot,
&framing::pane_frame_at(&anchors, aspect, camera),
);
// The root's yaw and scale, as [`sync_body_booth`] applies them: latched together, because
// both move on a resize.
let model_scale = framing::pane_root_scale_at(&anchors, display_aspect, camera);
if *last_pose != Some((yaw, model_scale)) {
commands.entity(booth.root).insert(
Transform::from_rotation(Quat::from_rotation_y(yaw))
.with_scale(Vec3::splat(model_scale)),
);
*last_pose = Some((yaw, model_scale));
booth.wake = booth.wake.max(BOOTH_SETTLE_FRAMES);
}
// The root, as [`sync_body_booth`] applies it: turn, scale and position latched together,
// because they are one transform and the scale moves on a resize.
let pose = RootPose {
yaw,
scale: framing::pane_root_scale_at(&anchors, display_aspect, camera),
pos: pane_position(positions, slot, *last_pose),
};
pose_booth_root(commands, booth, slot, pose, last_pose, false);
true
}
@@ -2550,7 +2639,7 @@ fn sync_body_booth(
yaw: f32,
camera: i32,
life: PaneLife,
last_pose: &mut Option<(f32, f32)>,
last_pose: &mut Option<RootPose>,
display_aspect: f32,
commands: &mut Commands,
booths: &mut Booths,
@@ -2563,6 +2652,7 @@ fn sync_body_booth(
cams: &mut Query<(&BoothCam, &mut Transform, &mut Projection)>,
anim_data: Option<&crate::creature_anim::AnimData>,
panes: &BoothPanes,
positions: &PanePositions,
) {
let Some(booth) = booths.0.get_mut(slot) else {
return;
@@ -2753,8 +2843,9 @@ fn sync_body_booth(
);
booth.snap = Some(key);
}
// The model root: the widget's `T(pos)·R(facing)·S(s)` with `pos` at the origin. `s`
// (`framing::pane_root_scale`) is latched with the yaw: both move on a resize.
// The model root: the widget's `T(pos)·R(facing)·S(s)`, all three latched together
// ([`RootPose`]) — `s` (`framing::pane_root_scale`) moves on a resize, `pos` whenever the
// pane calls `SetPosition`.
//
// The turn keys on the yaw alone, as `0x505bb0` picks the shuffle before storing the angle,
// and compares the Lua-facing scalar the reference passes to `SetRotation`.
@@ -2764,14 +2855,12 @@ fn sync_body_booth(
}
booth.turn.faced = Some(yaw);
}
if parts_changed || *last_pose != Some((yaw, model_scale)) {
commands.entity(booth.root).insert(
Transform::from_rotation(Quat::from_rotation_y(yaw))
.with_scale(Vec3::splat(model_scale)),
);
*last_pose = Some((yaw, model_scale));
booth.wake = booth.wake.max(BOOTH_SETTLE_FRAMES);
}
let pose = RootPose {
yaw,
scale: model_scale,
pos: pane_position(positions, slot, *last_pose),
};
pose_booth_root(commands, booth, slot, pose, last_pose, parts_changed);
}
/// What one frame owes a booth's pipeline settle ([`Booth::pipes_settling`]).
+115 -2
View File
@@ -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>();