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:
@@ -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 {
|
||||
|
||||
@@ -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`]).
|
||||
|
||||
@@ -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