The zoom limit is cameraDistanceMax × cameraDistanceMaxFactor held to [0, 50] by the reference's ordered compares, cameraDistanceMax a registered CVar, and a NaN factor zooms without panicking (#475)
The farthest zoom is now 1.12.1's `cameraDistanceMax × cameraDistanceMaxFactor`, held to 0 and 50 yd by the reference's two ordered compares (`0x5112d6`–`0x51131e`), and a NaN factor no longer crashes the first wheel notch. - `cameraDistanceMax` is registered, default "15.0" (`0x84fbd0`), with its validator's range: `0x50b310` → `0x50b330(v, 0.0, 50.0)` accepts 0 to 50 and refuses the rest, NaN included, keeping the old value. - `cameraDistanceMaxFactor` takes any value, as the reference registers it with no validator: `/console cameraDistanceMaxFactor 3` zooms to 45 yd. The options slider keeps its 1 to 2 range (`UIOptionsFrame.lua:90`). - Below 0 the limit is 0; at or above 50 it is 50; a NaN product compares unordered and gives 50, as the x87 flags do. The wheel bounds the distance with `max`/`min`, never `f32::clamp` on a CVar-fed bound. - A distance read back from the saved pose is capped at 50, not 30. - The zoom moves to its own module, `player/camera_zoom.rs`, keeping `camera.rs` under the line budget. Fixes #460 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0237dbd19b
commit
655854c154
@@ -403,8 +403,12 @@ pub(crate) const REGISTERED: &[Registered] = &[
|
||||
// The default matches; Deviation: here the dial multiplies the camera's own rate, because
|
||||
// benilla does not change the OS pointer speed, a system-wide setting.
|
||||
same("mousespeed", "1"),
|
||||
// MAX_FOLLOW_DIST (`UIOptionsFrame.lua:90`), a factor over `cameraDistanceMax`'s 15 yd
|
||||
// (`0x84fbd0`); registered "1.0" (`0x82e92c`).
|
||||
// The zoom-out limit is `cameraDistanceMax × cameraDistanceMaxFactor`, held to [0, 50]
|
||||
// (`0x5112d6`). `cameraDistanceMax` (`0x50beb2`, default `0x84fbd0`) has no panel row; its
|
||||
// validator `0x50b310` refuses a value outside [0, 50], and `player::camera::on_cvar` does the
|
||||
// same. The factor is MAX_FOLLOW_DIST (`UIOptionsFrame.lua:90`), registered "1.0" (`0x82e92c`)
|
||||
// with no validator: the slider's 1 to 2 is the slider's, not the CVar's.
|
||||
same("cameraDistanceMax", "15.0"),
|
||||
same("cameraDistanceMaxFactor", "1"),
|
||||
// `cameraSmoothStyle` (`0x50ba92`, default `[0x84f4f4]` "1"), the auto-return behind the
|
||||
// character. The engine's enum is 0 Never, 1 Smart, 2 Always, as the stock dropdown writes it
|
||||
@@ -1438,9 +1442,8 @@ mod tests {
|
||||
use crate::chat_bubble::BubbleConfig;
|
||||
use crate::minimap::MinimapZoom;
|
||||
use crate::nameplates::NameConfig;
|
||||
use crate::player::camera::{
|
||||
FollowConfig, FollowStyle, LookConfig, ZoomLimit, FOLLOW_SPEED_RANGE,
|
||||
};
|
||||
use crate::player::camera::{FollowConfig, FollowStyle, LookConfig, FOLLOW_SPEED_RANGE};
|
||||
use crate::player::camera_zoom::ZoomLimit;
|
||||
use crate::portrait::PaneRate;
|
||||
use crate::sound::SoundConfig;
|
||||
use crate::target::ClickConfig;
|
||||
@@ -1577,6 +1580,7 @@ mod tests {
|
||||
LookConfig::default().invert_pitch
|
||||
);
|
||||
assert_eq!(d["mousespeed"], LookConfig::default().sensitivity);
|
||||
assert_eq!(d["cameraDistanceMax"], ZoomLimit::default().distance_max());
|
||||
assert_eq!(d["cameraDistanceMaxFactor"], ZoomLimit::default().factor());
|
||||
let follow = FollowConfig::default();
|
||||
assert_eq!(
|
||||
@@ -1812,11 +1816,25 @@ mod tests {
|
||||
res::<FollowConfig>(&app).yaw_speed,
|
||||
*FOLLOW_SPEED_RANGE.end()
|
||||
);
|
||||
// The max-orbit factor lands as YARDS on the knob (base 15 x factor), clamped to 1..2.
|
||||
// The max orbit lands as yards on the knob: `cameraDistanceMax` x the factor, held to
|
||||
// [0, 50]; the factor is not held to its slider's 1 to 2.
|
||||
apply(&mut app, "cameraDistanceMaxFactor", "1");
|
||||
assert_eq!(res::<ZoomLimit>(&app).max, 15.0);
|
||||
apply(&mut app, "cameradistancemaxfactor", "5");
|
||||
assert_eq!(res::<ZoomLimit>(&app).max, 30.0);
|
||||
apply(&mut app, "cameradistancemaxfactor", "3");
|
||||
assert_eq!(res::<ZoomLimit>(&app).max, 45.0);
|
||||
apply(&mut app, "cameraDistanceMaxFactor", "1");
|
||||
apply(&mut app, "cameraDistanceMax", "25");
|
||||
assert_eq!(res::<ZoomLimit>(&app).max, 25.0);
|
||||
apply(&mut app, "cameraDistanceMaxFactor", "3");
|
||||
assert_eq!(res::<ZoomLimit>(&app).max, 50.0);
|
||||
apply(&mut app, "cameraDistanceMaxFactor", "-1");
|
||||
assert_eq!(res::<ZoomLimit>(&app).max, 0.0);
|
||||
apply(&mut app, "cameraDistanceMaxFactor", "nan");
|
||||
assert_eq!(res::<ZoomLimit>(&app).max, 50.0);
|
||||
// Out of `cameraDistanceMax`'s validator range: refused at the knob, 25 stands.
|
||||
apply(&mut app, "cameraDistanceMaxFactor", "1");
|
||||
apply(&mut app, "cameraDistanceMax", "60");
|
||||
assert_eq!(res::<ZoomLimit>(&app).max, 25.0);
|
||||
apply(&mut app, "autoLootDefault", "1");
|
||||
assert!(res::<LootConfig>(&app).auto_loot);
|
||||
apply(&mut app, "showLootSpam", "0");
|
||||
|
||||
@@ -30,6 +30,7 @@ mod body_pose;
|
||||
pub(crate) mod camera;
|
||||
mod camera_channel;
|
||||
pub(crate) mod camera_dynamics;
|
||||
pub(crate) mod camera_zoom;
|
||||
mod controller;
|
||||
mod world_focus;
|
||||
// The remembered camera pose, inside `player/` to read the rig's `pub(super)` fields.
|
||||
@@ -70,10 +71,9 @@ pub(crate) use camera::apply_self_model_fade;
|
||||
use controller::control;
|
||||
// The private camera and `state` items are re-imported for the modules beside this one, which
|
||||
// name them `super::X`.
|
||||
use camera::{
|
||||
apply_zoom_scroll, model_pivot_height, run_look_session, FlyCam, LookButton, CAM_DIST_DEFAULT,
|
||||
};
|
||||
pub(crate) use camera::{head_height, CameraControl, CameraPivot};
|
||||
use camera::{model_pivot_height, run_look_session, FlyCam, LookButton};
|
||||
use camera_zoom::{apply_zoom_scroll, CAM_DIST_DEFAULT};
|
||||
// `/follow`: chat sends the request, `crate::target` resolves the subject into the state, and
|
||||
// `follow` owns the motion.
|
||||
pub(crate) use follow::{FollowRequest, FollowState};
|
||||
@@ -224,7 +224,7 @@ impl Plugin for PlayerPlugin {
|
||||
app.add_observer(camera::on_cvar);
|
||||
app.add_observer(camera_dynamics::on_cvar);
|
||||
app.init_resource::<camera::LookConfig>();
|
||||
app.init_resource::<camera::ZoomLimit>();
|
||||
app.init_resource::<camera_zoom::ZoomLimit>();
|
||||
app.init_resource::<camera::FollowConfig>();
|
||||
app.init_resource::<camera_dynamics::CameraOptions>();
|
||||
// Far sight resolves `PLAYER_FARSIGHT` to a pose before `control` seats the camera: a
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! The third-person camera rig: the two mouse-look modes, the wheel-zoom glide, the
|
||||
//! collision-swept boom on the framing pivot, and the self-avatar fade into first person.
|
||||
//! The third-person camera rig: the two mouse-look modes, the collision-swept boom on the
|
||||
//! framing pivot, and the self-avatar fade into first person. The zoom is [`super::camera_zoom`].
|
||||
|
||||
use bevy::ecs::entity::EntityHashSet;
|
||||
use bevy::input::mouse::AccumulatedMouseMotion;
|
||||
@@ -9,6 +9,7 @@ use bevy::window::{CursorGrabMode, CursorOptions, PrimaryWindow};
|
||||
|
||||
use super::camera_channel::{Arm, SmoothChannel};
|
||||
use super::camera_dynamics::{DynamicsInput, HeadBob, SmartPivot, TerrainTilt};
|
||||
use super::camera_zoom::{ZoomLimit, CAM_DISTANCE_MAX_RANGE};
|
||||
use crate::creature_anim::wrap_pi;
|
||||
use crate::net::Embodied;
|
||||
use benilla_assets::materials::WowModelMaterial;
|
||||
@@ -57,53 +58,6 @@ impl PressGesture {
|
||||
}
|
||||
}
|
||||
|
||||
/// The zoom floor, yd: at 0 the eye sits at the framing pivot, inside the head, and the avatar
|
||||
/// fades out. The reference clamps the orbit to `cameraDistanceMax × cameraDistanceMaxFactor`,
|
||||
/// capped at 50 (`0x5112d0`). Deviation: the starting zoom, [`CAM_DIST_DEFAULT`], is 15 yd where
|
||||
/// the reference's `cameraDistance` is 5.55 (`0x84f488`), for a wider view.
|
||||
pub(super) const CAM_DIST_MIN: f32 = 0.0;
|
||||
/// The reference's `cameraDistanceMax` (default 15); 1.12's panel offers only the factor.
|
||||
pub(super) const CAM_DIST_BASE_MAX: f32 = 15.0;
|
||||
/// `cameraDistanceMaxFactor`'s slider, MAX_FOLLOW_DIST (1 to 2 by 0.1, `UIOptionsFrame.lua:90`).
|
||||
pub(crate) const CAM_DIST_FACTOR_RANGE: std::ops::RangeInclusive<f32> = 1.0..=2.0;
|
||||
/// The factor slider's top: the clamp for a distance read back off disk ([`ZoomLimit`] is live).
|
||||
pub(super) const CAM_DIST_MAX: f32 = CAM_DIST_BASE_MAX * 2.0;
|
||||
pub(super) const CAM_DIST_DEFAULT: f32 = 15.0;
|
||||
|
||||
/// The max orbit distance: 1.12's `cameraDistanceMaxFactor` over [`CAM_DIST_BASE_MAX`], 15 yd at
|
||||
/// the reference's defaults (`cameraDistanceMax` "15.0" at `0x84fbd0`, `cameraDistanceMaxFactor`
|
||||
/// "1.0" at `0x82e92c`). Lowering the factor pulls the live target in on the next frame.
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct ZoomLimit {
|
||||
pub(crate) max: f32,
|
||||
}
|
||||
|
||||
impl Default for ZoomLimit {
|
||||
fn default() -> Self {
|
||||
// Factor 1.0, the reference's default; `CAM_DIST_MAX` is the slider's top.
|
||||
Self {
|
||||
max: CAM_DIST_BASE_MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ZoomLimit {
|
||||
pub(crate) fn set_factor(&mut self, factor: f32) {
|
||||
let f = factor.clamp(*CAM_DIST_FACTOR_RANGE.start(), *CAM_DIST_FACTOR_RANGE.end());
|
||||
self.max = CAM_DIST_BASE_MAX * f;
|
||||
}
|
||||
|
||||
/// The live factor, the inverse of [`Self::set_factor`], for tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn factor(&self) -> f32 {
|
||||
self.max / CAM_DIST_BASE_MAX
|
||||
}
|
||||
}
|
||||
/// Yards per wheel notch, the stock bindings' `CameraZoomIn(1.0)` (`Bindings.xml:707`).
|
||||
const CAM_ZOOM_STEP: f32 = 1.0;
|
||||
/// Zoom speed in yd/s, `cameraDistanceMoveSpeed`'s default: the reference glides the distance to
|
||||
/// the wheel target at this constant speed (`0x5112d0`), not an ease.
|
||||
const CAM_MOVE_SPEED: f32 = 8.33;
|
||||
/// Radians of camera rotation per raw mouse unit at the default move speeds and `mousespeed` 1.0.
|
||||
const LOOK_SENSITIVITY: f32 = 0.003;
|
||||
/// `mousespeed`'s slider, MOUSE_SENSITIVITY (0.5 to 1.5 by 0.05, `UIOptionsFrame.lua:87`).
|
||||
@@ -138,6 +92,16 @@ pub(crate) fn on_cvar(
|
||||
look.pitch_speed = v;
|
||||
}
|
||||
}
|
||||
"cameradistancemax" => {
|
||||
if !zoom.set_distance_max(v) {
|
||||
warn!(
|
||||
"cvar {}: value out of range ({} - {}) — ignored",
|
||||
ev.name,
|
||||
CAM_DISTANCE_MAX_RANGE.start(),
|
||||
CAM_DISTANCE_MAX_RANGE.end()
|
||||
);
|
||||
}
|
||||
}
|
||||
"cameradistancemaxfactor" => zoom.set_factor(v),
|
||||
// The stock dropdown writes 1 Smart, 2 Always, 0 Never (`UIOptionsFrame.lua:518`).
|
||||
"camerasmoothstyle" => follow.style = FollowStyle::from_cvar(v),
|
||||
@@ -880,20 +844,6 @@ pub(super) fn run_look_session(
|
||||
rig.freelook = rig.look == Some(LookButton::Right) || (rig.look.is_some() && both_buttons);
|
||||
}
|
||||
|
||||
/// Wheel zoom, run every frame: `CAMERAZOOMIN`/`OUT` move the target, and the distance glides to it
|
||||
/// at a constant `cameraDistanceMoveSpeed`, as the reference's does. `scroll` is this frame's net
|
||||
/// zoom-in in notches (positive is closer).
|
||||
pub(super) fn apply_zoom_scroll(scroll: f32, dt: f32, rig: &mut CameraControl, max: f32) {
|
||||
if scroll != 0.0 {
|
||||
rig.target_distance =
|
||||
(rig.target_distance - scroll * CAM_ZOOM_STEP).clamp(CAM_DIST_MIN, max);
|
||||
}
|
||||
// Re-clamp every frame, so lowering the max-distance slider pulls the camera in.
|
||||
rig.target_distance = rig.target_distance.min(max);
|
||||
let max_step = CAM_MOVE_SPEED * dt;
|
||||
rig.distance += (rig.target_distance - rig.distance).clamp(-max_step, max_step);
|
||||
}
|
||||
|
||||
/// Seat the camera on whatever it orbits: our own body, or the far-sight subject `PLAYER_FARSIGHT`
|
||||
/// names (Mind Vision, Sentry Totem, Mind Control), substituting the orbit centre, sweep origin and
|
||||
/// pivot target before [`seat_camera`]. Both of the controller's seat paths call it, as far sight
|
||||
|
||||
@@ -18,9 +18,8 @@ use bevy::prelude::*;
|
||||
|
||||
use crate::char_select::{ClientState, InWorldGated};
|
||||
|
||||
use super::camera::{
|
||||
CameraControl, FlyCam, CAM_DIST_DEFAULT, CAM_DIST_MAX, CAM_DIST_MIN, CAM_PITCH_LIMIT,
|
||||
};
|
||||
use super::camera::{CameraControl, FlyCam, CAM_PITCH_LIMIT};
|
||||
use super::camera_zoom::{CAM_DIST_DEFAULT, CAM_DIST_MAX, CAM_DIST_MIN};
|
||||
use super::Player;
|
||||
use benilla_world::view::WorldCamera;
|
||||
|
||||
|
||||
@@ -25,8 +25,9 @@ use benilla_world::view::WorldCamera;
|
||||
|
||||
use crate::creature_anim::wrap_pi;
|
||||
|
||||
use super::camera::{CameraControl, FlyCam, CAM_DIST_MAX, CAM_DIST_MIN, CAM_PITCH_LIMIT};
|
||||
use super::camera::{CameraControl, FlyCam, CAM_PITCH_LIMIT};
|
||||
use super::camera_saved::{pitch_from_file, pitch_to_file};
|
||||
use super::camera_zoom::{CAM_DIST_MAX, CAM_DIST_MIN};
|
||||
use super::Player;
|
||||
|
||||
/// The reference's five views, through the UI crate's constant so the Lua range check agrees.
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
//! The zoom: the orbit distance's range, from `cameraDistanceMax` × `cameraDistanceMaxFactor`
|
||||
//! under the reference's 0 and 50 yd bounds, and the wheel's glide to the chosen distance.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use super::camera::CameraControl;
|
||||
|
||||
/// The zoom floor, yd (`ds:0x8089a8` = 0.0): at 0 the eye sits at the framing pivot, inside the
|
||||
/// head, and the avatar fades out. Deviation: the starting zoom, [`CAM_DIST_DEFAULT`], is 15 yd
|
||||
/// where the reference's `cameraDistance` is 5.55 (`0x84f488`), for a wider view.
|
||||
pub(super) const CAM_DIST_MIN: f32 = 0.0;
|
||||
/// The zoom ceiling, yd (`ds:0x8089a4` = 50.0): the cap on [`ZoomLimit`] (`0x511309`) and the top
|
||||
/// of the distance validator `0x50b310`; also the clamp for a distance read back off disk.
|
||||
pub(super) const CAM_DIST_MAX: f32 = 50.0;
|
||||
pub(super) const CAM_DIST_DEFAULT: f32 = 15.0;
|
||||
/// `cameraDistanceMax`'s registered default, "15.0" (`0x84fbd0`).
|
||||
const CAM_DISTANCE_MAX_DEFAULT: f32 = 15.0;
|
||||
/// `cameraDistanceMaxFactor`'s registered default, "1.0" (`0x82e92c`). The CVar has no validator:
|
||||
/// the options slider's 1 to 2 (`UIOptionsFrame.lua:90`) bounds the slider, not the value.
|
||||
const CAM_DISTANCE_MAX_FACTOR_DEFAULT: f32 = 1.0;
|
||||
/// `cameraDistanceMax`'s validator (`0x50b310` → `0x50b330(v, 0.0, 50.0)`) accepts `0 ≤ v ≤ 50`;
|
||||
/// anything else, NaN included (`fcom` unordered sets C0), is refused and the old value stands.
|
||||
pub(crate) const CAM_DISTANCE_MAX_RANGE: std::ops::RangeInclusive<f32> =
|
||||
CAM_DIST_MIN..=CAM_DIST_MAX;
|
||||
/// Yards per wheel notch, the stock bindings' `CameraZoomIn(1.0)` (`Bindings.xml:707`).
|
||||
const CAM_ZOOM_STEP: f32 = 1.0;
|
||||
/// Zoom speed in yd/s, `cameraDistanceMoveSpeed`'s default: the reference glides the distance to
|
||||
/// the wheel target at this constant speed (`0x5112d0`), not an ease.
|
||||
const CAM_MOVE_SPEED: f32 = 8.33;
|
||||
|
||||
/// The max orbit distance, yd, from `cameraDistanceMax` and `cameraDistanceMaxFactor`: 15 at the
|
||||
/// reference's defaults. Lowering it pulls the live target in on the next frame.
|
||||
#[derive(Resource)]
|
||||
pub(crate) struct ZoomLimit {
|
||||
/// The live limit, [`orbit_limit`] of the two below: in `[0, 50]`, never NaN.
|
||||
pub(crate) max: f32,
|
||||
distance_max: f32,
|
||||
factor: f32,
|
||||
}
|
||||
|
||||
impl Default for ZoomLimit {
|
||||
fn default() -> Self {
|
||||
let (distance_max, factor) = (CAM_DISTANCE_MAX_DEFAULT, CAM_DISTANCE_MAX_FACTOR_DEFAULT);
|
||||
Self {
|
||||
max: orbit_limit(distance_max, factor),
|
||||
distance_max,
|
||||
factor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ZoomLimit {
|
||||
/// `cameraDistanceMax`, through its validator's range ([`CAM_DISTANCE_MAX_RANGE`]); `false`
|
||||
/// when refused, the old value standing.
|
||||
pub(crate) fn set_distance_max(&mut self, v: f32) -> bool {
|
||||
if !CAM_DISTANCE_MAX_RANGE.contains(&v) {
|
||||
return false;
|
||||
}
|
||||
self.distance_max = v;
|
||||
self.max = orbit_limit(self.distance_max, self.factor);
|
||||
true
|
||||
}
|
||||
|
||||
/// `cameraDistanceMaxFactor`, taken as it comes: the reference registers it with no validator.
|
||||
pub(crate) fn set_factor(&mut self, factor: f32) {
|
||||
self.factor = factor;
|
||||
self.max = orbit_limit(self.distance_max, self.factor);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn distance_max(&self) -> f32 {
|
||||
self.distance_max
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn factor(&self) -> f32 {
|
||||
self.factor
|
||||
}
|
||||
}
|
||||
|
||||
/// The reference's orbit limit (`0x5112d6`–`0x51131e`): `cameraDistanceMax × cameraDistanceMaxFactor`
|
||||
/// through two ordered x87 compares. Below 0.0 (`fcom ds:0x8089a8`, C0 alone) it is 0.0; else
|
||||
/// below 50.0 (`fcom ds:0x8089a4`, C0 alone) it stands; else it is 50.0. A NaN product compares
|
||||
/// unordered (C0, C2 and C3 all set), which passes the first test and fails the second: 50.0.
|
||||
fn orbit_limit(distance_max: f32, factor: f32) -> f32 {
|
||||
let product = distance_max * factor;
|
||||
if product < CAM_DIST_MIN {
|
||||
CAM_DIST_MIN
|
||||
} else if product < CAM_DIST_MAX {
|
||||
product
|
||||
} else {
|
||||
CAM_DIST_MAX
|
||||
}
|
||||
}
|
||||
|
||||
/// Wheel zoom, run every frame: `CAMERAZOOMIN`/`OUT` move the target, and the distance glides to it
|
||||
/// at a constant `cameraDistanceMoveSpeed`, as the reference's does. `scroll` is this frame's net
|
||||
/// zoom-in in notches (positive is closer). `max` is [`ZoomLimit::max`], CVar-fed, so it bounds by
|
||||
/// `max`/`min`, never `f32::clamp`, which panics on a NaN or inverted bound.
|
||||
pub(super) fn apply_zoom_scroll(scroll: f32, dt: f32, rig: &mut CameraControl, max: f32) {
|
||||
if scroll != 0.0 {
|
||||
rig.target_distance = (rig.target_distance - scroll * CAM_ZOOM_STEP)
|
||||
.max(CAM_DIST_MIN)
|
||||
.min(max);
|
||||
}
|
||||
// Re-clamp every frame, so lowering the max-distance slider pulls the camera in.
|
||||
rig.target_distance = rig.target_distance.min(max);
|
||||
let max_step = CAM_MOVE_SPEED * dt;
|
||||
rig.distance += (rig.target_distance - rig.distance).clamp(-max_step, max_step);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn limit(distance_max: f32, factor: f32) -> f32 {
|
||||
let mut z = ZoomLimit::default();
|
||||
assert!(
|
||||
z.set_distance_max(distance_max),
|
||||
"{distance_max} is in range"
|
||||
);
|
||||
z.set_factor(factor);
|
||||
z.max
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_limit_is_the_product_of_both_cvars() {
|
||||
assert_eq!(ZoomLimit::default().max, 15.0, "the reference's defaults");
|
||||
// The factor has no validator: 3 is not held to the slider's 2.
|
||||
assert_eq!(limit(15.0, 3.0), 45.0);
|
||||
assert_eq!(limit(15.0, 0.5), 7.5, "nor raised to the slider's 1");
|
||||
assert_eq!(limit(25.0, 1.0), 25.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_limit_holds_to_zero_and_fifty() {
|
||||
assert_eq!(limit(15.0, 4.0), 50.0);
|
||||
assert_eq!(limit(50.0, 1.0), 50.0);
|
||||
assert_eq!(limit(15.0, f32::INFINITY), 50.0);
|
||||
assert_eq!(limit(15.0, -2.0), 0.0);
|
||||
assert_eq!(limit(15.0, f32::NEG_INFINITY), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_nan_product_fails_both_compares_and_is_fifty() {
|
||||
assert_eq!(limit(15.0, f32::NAN), 50.0);
|
||||
// 0 × ∞ is NaN too, with both values accepted.
|
||||
assert_eq!(limit(0.0, f32::INFINITY), 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cameradistancemax_is_refused_outside_its_validator_range() {
|
||||
let mut z = ZoomLimit::default();
|
||||
for bad in [-0.5, 50.5, f32::NAN, f32::INFINITY] {
|
||||
assert!(!z.set_distance_max(bad), "{bad} is refused");
|
||||
assert_eq!(
|
||||
(z.distance_max(), z.max),
|
||||
(15.0, 15.0),
|
||||
"the old value stands"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
z.set_distance_max(0.0) && z.max == 0.0,
|
||||
"both ends are inclusive"
|
||||
);
|
||||
assert!(z.set_distance_max(50.0) && z.max == 50.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_nan_factor_zooms_without_panicking() {
|
||||
let mut z = ZoomLimit::default();
|
||||
z.set_factor(f32::NAN);
|
||||
let mut rig = CameraControl {
|
||||
target_distance: 10.0,
|
||||
distance: 10.0,
|
||||
..default()
|
||||
};
|
||||
apply_zoom_scroll(-1.0, 0.1, &mut rig, z.max);
|
||||
assert_eq!(rig.target_distance, 11.0);
|
||||
// Even a NaN bound handed straight in bounds nothing and panics nowhere.
|
||||
apply_zoom_scroll(-1.0, 0.1, &mut rig, f32::NAN);
|
||||
assert_eq!(rig.target_distance, 12.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_wheel_stops_at_the_limit() {
|
||||
let mut rig = CameraControl {
|
||||
target_distance: 44.5,
|
||||
..default()
|
||||
};
|
||||
apply_zoom_scroll(-1.0, 0.1, &mut rig, limit(15.0, 3.0));
|
||||
assert_eq!(rig.target_distance, 45.0);
|
||||
apply_zoom_scroll(1000.0, 0.1, &mut rig, 45.0);
|
||||
assert_eq!(rig.target_distance, CAM_DIST_MIN);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ pub(super) fn control(
|
||||
pointer: (
|
||||
Res<AccumulatedMouseMotion>,
|
||||
Res<camera::LookConfig>,
|
||||
Res<camera::ZoomLimit>,
|
||||
Res<camera_zoom::ZoomLimit>,
|
||||
Res<camera::FollowConfig>,
|
||||
Res<camera_dynamics::CameraOptions>,
|
||||
// Only `nearclip` is read here, for the self-avatar fade.
|
||||
|
||||
Reference in New Issue
Block a user