forked from 1b-agent/opencadstudio-upstream
fix(view): hold ZOOMFACTOR to its own range, finish the exponential zoom
The exponential camera step is the right call. `0.9 ^ delta` is strictly
positive for every real delta, in and out cancel exactly where the
subtraction lost a percent of the view on every round trip, and the file's
own header has promised an "exponential feel" since it was written. Three
surfaces were left behind it, and the wider range exposed a fourth thing.
The range of the system variable. 3 to 100 is the range ZOOMFACTOR has, and
the User Preferences page was built against it; `SETVAR ZOOMFACTOR` accepts
that and nothing else again. A faster wheel is the Options window's business
instead: the slider covers the variable's own range, the field beside it
reaches 500, and a read-out says what a notch actually does ("10.0% a
notch") so the number being set is readable rather than implied. A value
above 100 therefore lives only in this application's own configuration file,
where it is nobody else's to read, and a bare `SETVAR ZOOMFACTOR` still
reports it. The bounds and the clamp live in one place now
(`settings::clamp_zoom_factor`): four literal copies of `3, 100` are what
let a surface be missed in the first place.
A floating viewport in MSPACE. It zooms its own view through
`zoom_active_viewport`, which kept subtracting: the factor reached zero at
6.67 steps, so from ZOOMFACTOR 360 up every notch inside a viewport bottomed
out on the clamp — a tenfold jump a notch, and the top third of the slider
doing nothing at all. Exponential there too, from a base of 0.85 so the
default notch still moves a viewport the 15% it always has. Each surface
keeps its own long-standing feel rather than inheriting the other's.
The trackpad pinch. `pinch_zoom_steps` was calibrated against the
subtraction (`1 - step / 10 = 1 / (1 + m)`) so that a pinch of m leaves the
view exactly 1 + m times closer — the property docs/trackpad.md documents
and the reason the gesture tracks the fingers at all. Inverting the zoom law
by hand is what let the two drift apart, so the step is asked of the camera
now (`Camera::zoom_steps_for_ratio`), which cannot drift by construction.
`pinch_is_one_to_one` recomputed the old formula rather than the camera's,
so it kept passing through all of this; it zooms a real camera and measures
the distance it lands at instead.
Also: `zoom` had a floor and no ceiling, and zooming out multiplies. At
ZOOMFACTOR 500 a notch out is x2.4, so about ninety notches — a couple of
seconds of scrolling — ran the distance past what an f32 far plane
(`distance * 1000`) can hold, and the viewport went black on an infinite
projection. A degenerate pinch magnification of -1 or beyond reached the
same place through the other side. Both are bounded now.
Also: two doc comments quoted the linear law (`zoom_view_at_cursor` and the
one on `pinch_zoom_steps`), and docs/trackpad.md stated the pinch identity
and claimed the tests covered it against the camera. All updated, with what
a notch does and where each surface's range comes from written down.
Tests, each confirmed against the behaviour it describes: the zoom law at
every factor the application can hold (positive, monotonic in the setting,
in and out cancelling), the ceiling under a long scroll out in both
projections, the published inverse and what it does with a nonsense ratio,
the viewport factor and a real viewport zoomed three notches at the top of
the range, the SETVAR range including a value the Options field set, the
Options field (above 100, above the ceiling, cleared, not a number), its
agreement with the slider, Close putting both the value and the field back,
and the read-out's percentages. 1882 lib tests pass.
The two new Options strings are not in the locale catalog yet, so they
render in English until it is regenerated.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
558b5f6526
commit
9fe2e4fa58
12 files changed
+549
-32
No files matched your search
@@ -1687,13 +1687,23 @@ impl OpenCADStudio {
|
||||
false,
|
||||
)),
|
||||
},
|
||||
// The system variable keeps its own range. The
|
||||
// Options window reaches further
|
||||
// (`settings::ZOOM_FACTOR_MAX`) and may have
|
||||
// stored a value above this, which a bare
|
||||
// `SETVAR ZOOMFACTOR` still reports; only setting
|
||||
// one is held to the range of the variable.
|
||||
"ZOOMFACTOR" => match &value {
|
||||
Some(v) => match v.parse::<i32>() {
|
||||
Ok(factor) if (3..=500).contains(&factor) => {
|
||||
Ok(factor)
|
||||
if (crate::app::settings::ZOOM_FACTOR_MIN
|
||||
..=crate::app::settings::ZOOM_FACTOR_SYSVAR_MAX)
|
||||
.contains(&factor) =>
|
||||
{
|
||||
self.zoom_factor = factor;
|
||||
Ok((format!("ZOOMFACTOR = {factor}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 3 to 500 required.".into()),
|
||||
_ => Err("SETVAR: integer from 3 to 100 required.".into()),
|
||||
},
|
||||
None => {
|
||||
Ok((format!("ZOOMFACTOR = {}", self.zoom_factor), false))
|
||||
@@ -3475,6 +3485,31 @@ mod tests {
|
||||
app
|
||||
}
|
||||
|
||||
/// `SETVAR ZOOMFACTOR` is the system variable, so it keeps the range
|
||||
/// the variable has — the Options window is where a wheel faster than
|
||||
/// that is set, and a value set there is still what the variable
|
||||
/// reports.
|
||||
#[test]
|
||||
fn setvar_zoomfactor_keeps_the_range_of_the_system_variable() {
|
||||
let mut app = fresh_app();
|
||||
|
||||
let _ = app.run_command_line("SETVAR ZOOMFACTOR 100");
|
||||
assert_eq!(app.zoom_factor, 100);
|
||||
|
||||
let _ = app.run_command_line("SETVAR ZOOMFACTOR 101");
|
||||
assert_eq!(app.zoom_factor, 100, "101 is not a value the variable takes");
|
||||
|
||||
let _ = app.run_command_line("SETVAR ZOOMFACTOR 2");
|
||||
assert_eq!(app.zoom_factor, 100, "and neither is 2");
|
||||
|
||||
// What the Options field reached is still reported here, and the
|
||||
// command line can still set any value the variable does have.
|
||||
let _ = app.update(crate::app::Message::ZoomFactorInputChanged("250".into()));
|
||||
assert_eq!(app.zoom_factor, 250);
|
||||
let _ = app.run_command_line("SETVAR ZOOMFACTOR 60");
|
||||
assert_eq!(app.zoom_factor, 60);
|
||||
}
|
||||
|
||||
/// `SETVAR ISOLINES` stored any parseable `i16` verbatim. The header
|
||||
/// value is the per-face isolate count handed to the tessellation
|
||||
/// kernel (which loops `0..count`), so `ISOLINES 32767` made every
|
||||
|
||||
+15
-2
@@ -557,8 +557,15 @@ pub(super) struct OpenCADStudio {
|
||||
polar_increment_deg: f32,
|
||||
/// Reverse the mouse-wheel zoom direction when true (ZOOMWHEEL = 1).
|
||||
zoom_wheel_reversed: bool,
|
||||
/// Mouse-wheel zoom sensitivity, clamped to 3..=500 (ZOOMFACTOR).
|
||||
/// Mouse-wheel zoom sensitivity (ZOOMFACTOR), held inside
|
||||
/// `settings::ZOOM_FACTOR_MIN..=settings::ZOOM_FACTOR_MAX`. `SETVAR`
|
||||
/// only sets the range the system variable itself has; the Options
|
||||
/// window reaches the rest.
|
||||
zoom_factor: i32,
|
||||
/// The Options field behind `zoom_factor`. A text field is edited as
|
||||
/// text — cleared, half-typed — so the buffer is what the user sees and
|
||||
/// the value follows only once it parses.
|
||||
zoom_factor_input: String,
|
||||
/// Crosshair size setting (CURSORSIZE, 1..=100).
|
||||
cursor_size: i32,
|
||||
/// Selection-box size setting (PICKBOX, 0..=50).
|
||||
@@ -2322,8 +2329,13 @@ pub enum Message {
|
||||
CommandLineFadeChanged(i32),
|
||||
/// Toggle reversing the mouse-wheel zoom direction (ZOOMWHEEL).
|
||||
ZoomWheelReversedChanged(bool),
|
||||
/// Change how far one wheel notch zooms (ZOOMFACTOR, 3..=500).
|
||||
/// Change how far one wheel notch zooms, from the Options slider
|
||||
/// (ZOOMFACTOR).
|
||||
ZoomFactorChanged(i32),
|
||||
/// Edit the Options zoom-factor field; committed when it holds a number,
|
||||
/// which is how a value above the system variable's own range is set
|
||||
/// (ZOOMFACTOR).
|
||||
ZoomFactorInputChanged(String),
|
||||
/// Options → User Preferences: right-click behaviour (SHORTCUTMENU).
|
||||
RightClickModeChanged(settings::RightClickMode),
|
||||
/// Options → User Preferences: time-sensitive hold threshold, ms.
|
||||
@@ -4094,6 +4106,7 @@ impl OpenCADStudio {
|
||||
polar_increment_deg: 45.0,
|
||||
zoom_wheel_reversed: false,
|
||||
zoom_factor: 60,
|
||||
zoom_factor_input: 60.to_string(),
|
||||
cursor_size: 5,
|
||||
pick_box: 3,
|
||||
double_click_block_refedit: false,
|
||||
|
||||
@@ -140,7 +140,7 @@ impl OpenCADStudio {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::{Message, OpenCADStudio};
|
||||
use crate::app::{settings, Message, OpenCADStudio};
|
||||
|
||||
fn app() -> OpenCADStudio {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
@@ -223,14 +223,60 @@ mod tests {
|
||||
assert!(!app.tabs[i].dirty, "the drawing is as unmodified as it was");
|
||||
}
|
||||
|
||||
/// The Options window is the surface that reaches past the range the
|
||||
/// ZOOMFACTOR system variable has, and its field is what reaches: a
|
||||
/// number above the variable's own ceiling is kept, one above what the
|
||||
/// application can store is held there, and text that is not yet a
|
||||
/// number leaves the setting alone so the field can be cleared and
|
||||
/// retyped.
|
||||
#[test]
|
||||
fn zoom_factor_supports_values_above_100() {
|
||||
fn the_zoom_factor_field_reaches_past_the_system_variable() {
|
||||
let mut app = app();
|
||||
let _ = app.update(Message::ZoomFactorChanged(250));
|
||||
let _ = app.update(Message::ZoomFactorInputChanged("250".into()));
|
||||
assert_eq!(app.zoom_factor, 250);
|
||||
let _ = app.update(Message::ZoomFactorChanged(500));
|
||||
assert_eq!(app.zoom_factor, 500);
|
||||
let _ = app.update(Message::ZoomFactorChanged(600));
|
||||
assert_eq!(app.zoom_factor, 500, "clamped to new upper limit of 500");
|
||||
|
||||
let _ = app.update(Message::ZoomFactorInputChanged("600".into()));
|
||||
assert_eq!(app.zoom_factor, settings::ZOOM_FACTOR_MAX);
|
||||
|
||||
let _ = app.update(Message::ZoomFactorInputChanged(String::new()));
|
||||
assert_eq!(
|
||||
app.zoom_factor,
|
||||
settings::ZOOM_FACTOR_MAX,
|
||||
"a cleared field keeps the value it set"
|
||||
);
|
||||
assert_eq!(app.zoom_factor_input, "", "while the field stays as typed");
|
||||
|
||||
let _ = app.update(Message::ZoomFactorInputChanged("nonsense".into()));
|
||||
assert_eq!(app.zoom_factor, settings::ZOOM_FACTOR_MAX);
|
||||
|
||||
let _ = app.update(Message::ZoomFactorInputChanged("1".into()));
|
||||
assert_eq!(app.zoom_factor, settings::ZOOM_FACTOR_MIN, "the floor holds");
|
||||
}
|
||||
|
||||
/// The slider and the field are two views of one setting: dragging the
|
||||
/// slider has to leave the number beside it reading what it just set, or
|
||||
/// the window shows two different answers for what the wheel does.
|
||||
#[test]
|
||||
fn the_zoom_factor_slider_and_field_stay_in_step() {
|
||||
let mut app = app();
|
||||
let _ = app.update(Message::ZoomFactorInputChanged("250".into()));
|
||||
let _ = app.update(Message::ZoomFactorChanged(40));
|
||||
assert_eq!(app.zoom_factor, 40);
|
||||
assert_eq!(app.zoom_factor_input, "40");
|
||||
}
|
||||
|
||||
/// Close puts a zoom factor back like every other preference — the
|
||||
/// field's text included, since it is edited in front of the value.
|
||||
#[test]
|
||||
fn closing_puts_the_zoom_factor_and_its_field_back() {
|
||||
let mut app = app();
|
||||
let _ = app.update(Message::OptionsOpen);
|
||||
let before = app.zoom_factor;
|
||||
let _ = app.update(Message::ZoomFactorInputChanged("250".into()));
|
||||
assert!(app.options_dirty());
|
||||
let _ = app.update(Message::OptionsClose);
|
||||
let _ = app.update(Message::OptionsCloseDiscard);
|
||||
assert_eq!(app.zoom_factor, before);
|
||||
assert_eq!(app.zoom_factor_input, before.to_string());
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,43 @@ pub fn clamp_right_click_hold_ms(v: i32) -> i32 {
|
||||
v.clamp(100, 1000)
|
||||
}
|
||||
|
||||
/// ZOOMFACTOR bounds.
|
||||
///
|
||||
/// `ZOOM_FACTOR_SYSVAR_MAX` is the range the system variable has: `SETVAR
|
||||
/// ZOOMFACTOR` accepts 3 to 100 and nothing else, so a drawing session set
|
||||
/// up on the command line stays within the range the variable has everywhere
|
||||
/// else. The Options window is the app's own surface and reaches
|
||||
/// `ZOOM_FACTOR_MAX` for anyone who wants a faster wheel than the variable
|
||||
/// can express; that value lives only in this application's own
|
||||
/// configuration file, so it is nobody else's to read.
|
||||
pub const ZOOM_FACTOR_MIN: i32 = 3;
|
||||
pub const ZOOM_FACTOR_SYSVAR_MAX: i32 = 100;
|
||||
pub const ZOOM_FACTOR_MAX: i32 = 500;
|
||||
|
||||
/// Hold a ZOOMFACTOR inside the range the application can store, wherever it
|
||||
/// arrives from — the slider, the Options field, or a configuration file
|
||||
/// written by hand or by an older build.
|
||||
pub fn clamp_zoom_factor(v: i32) -> i32 {
|
||||
v.clamp(ZOOM_FACTOR_MIN, ZOOM_FACTOR_MAX)
|
||||
}
|
||||
|
||||
/// The `Camera::zoom` step one wheel notch means at `factor`. The default 60
|
||||
/// is one whole step, which is what makes 60 the setting that leaves the
|
||||
/// camera's own `ZOOM_STEP` untouched.
|
||||
pub fn zoom_notch_steps(factor: i32) -> f32 {
|
||||
factor as f32 / 60.0
|
||||
}
|
||||
|
||||
/// How much nearer one wheel notch brings the view at `factor`, as a
|
||||
/// percentage — the number the Options window shows, so that what the slider
|
||||
/// and the field are setting is readable instead of implied. Derived from the
|
||||
/// camera's own law, never from a second copy of it.
|
||||
pub fn zoom_notch_percent(factor: i32) -> f32 {
|
||||
let ratio = crate::scene::view::camera::Camera::ZOOM_STEP
|
||||
.powf(zoom_notch_steps(clamp_zoom_factor(factor)));
|
||||
(1.0 - ratio) * 100.0
|
||||
}
|
||||
|
||||
/// Active pair of axes while isometric drafting is enabled.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum IsoPlane {
|
||||
@@ -816,6 +853,33 @@ impl Default for UserSettings {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The Options read-out has to say what a notch really does, or it is
|
||||
/// worse than no read-out. These are the percentages of the law in
|
||||
/// `Camera::zoom`, which is where the number comes from: the default a
|
||||
/// tenth of the distance, the top of the system variable's range a
|
||||
/// sixth, and the top of the application's range a little over half.
|
||||
#[test]
|
||||
fn the_zoom_read_out_says_what_a_notch_does() {
|
||||
for (factor, percent) in [(3, 0.53_f32), (60, 10.0), (100, 16.1), (500, 58.4)] {
|
||||
let shown = zoom_notch_percent(factor);
|
||||
assert!(
|
||||
(shown - percent).abs() < 0.05,
|
||||
"ZOOMFACTOR {factor} reads {shown}%, not {percent}%"
|
||||
);
|
||||
}
|
||||
// A value from outside the range reads as the one that will be used.
|
||||
assert_eq!(zoom_notch_percent(900), zoom_notch_percent(ZOOM_FACTOR_MAX));
|
||||
assert_eq!(zoom_notch_percent(0), zoom_notch_percent(ZOOM_FACTOR_MIN));
|
||||
}
|
||||
|
||||
/// The default is one whole step, which is the whole reason the camera's
|
||||
/// base is the per-notch ratio and not something scaled.
|
||||
#[test]
|
||||
fn the_default_zoom_factor_is_one_step() {
|
||||
assert_eq!(zoom_notch_steps(60), 1.0);
|
||||
assert_eq!(UserSettings::default().zoom_factor, 60);
|
||||
}
|
||||
|
||||
/// Object snap ships live. The modes were always pre-selected; only the
|
||||
/// master switch was off, so a new user got a configured snap set that
|
||||
/// did nothing until they found the status-bar pill.
|
||||
|
||||
@@ -751,7 +751,12 @@ impl OpenCADStudio {
|
||||
self.polar_mode = s.polar;
|
||||
self.polar_increment_deg = s.polar_increment_deg;
|
||||
self.zoom_wheel_reversed = s.zoom_wheel_reversed;
|
||||
self.zoom_factor = s.zoom_factor.clamp(3, 500);
|
||||
self.zoom_factor = crate::app::settings::clamp_zoom_factor(s.zoom_factor);
|
||||
// Like the drafting-rotation field below, the Options field edits a
|
||||
// buffer rather than the value, so it is reseeded whenever the value
|
||||
// is restored from behind it — a start-up, or the Options window's
|
||||
// own Close putting a change back.
|
||||
self.zoom_factor_input = self.zoom_factor.to_string();
|
||||
self.cursor_size = s.cursor_size.clamp(1, 100);
|
||||
self.pick_box = s.pick_box.clamp(0, 50);
|
||||
self.options_tab = s.options_tab;
|
||||
|
||||
+17
-1
@@ -8247,11 +8247,27 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
Message::ZoomFactorChanged(factor) => {
|
||||
self.zoom_factor = factor.clamp(3, 500);
|
||||
self.zoom_factor = crate::app::settings::clamp_zoom_factor(factor);
|
||||
// The field beside the slider reads the same value, so the
|
||||
// two never disagree about what the wheel is set to.
|
||||
self.zoom_factor_input = self.zoom_factor.to_string();
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
// Typed into freely; only committed when it parses, so clearing
|
||||
// the field to retype does not drop the wheel to its minimum.
|
||||
// This is the surface that reaches past the range the system
|
||||
// variable has.
|
||||
Message::ZoomFactorInputChanged(value) => {
|
||||
self.zoom_factor_input = value;
|
||||
if let Ok(factor) = self.zoom_factor_input.trim().parse::<i32>() {
|
||||
self.zoom_factor = crate::app::settings::clamp_zoom_factor(factor);
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::RightClickModeChanged(mode) => {
|
||||
self.right_click_mode = mode;
|
||||
self.persist_settings_if_changed();
|
||||
|
||||
@@ -76,9 +76,18 @@ fn scroll_intent(delta: mouse::ScrollDelta) -> ScrollIntent {
|
||||
/// magnification delta (0.05 = the fingers moved 5% further apart).
|
||||
///
|
||||
/// AppKit reports the increment of a scale factor, and a 5% wider pinch has to
|
||||
/// leave the view 5% closer, so the step solves `1 - step / 10 = 1 / (1 + m)`.
|
||||
/// leave the view 5% closer, so the step is the one that scales the camera
|
||||
/// distance by exactly `1 / (1 + m)`. The camera is asked for that step
|
||||
/// instead of being told it: inverting the zoom law here by hand (it read
|
||||
/// `1 - step / 10 = 1 / (1 + m)`) is what made the pinch stop tracking the
|
||||
/// fingers the moment the law itself changed.
|
||||
///
|
||||
/// The ratio is bounded because a magnification of -1 or beyond — which the
|
||||
/// gesture should never report — has no finite answer, and because no single
|
||||
/// gesture event has any business moving the view a hundredfold.
|
||||
fn pinch_zoom_steps(magnification: f32) -> f32 {
|
||||
10.0 * magnification / (1.0 + magnification)
|
||||
let ratio = (1.0 / (1.0 + magnification)).clamp(0.01, 100.0);
|
||||
crate::scene::view::camera::Camera::zoom_steps_for_ratio(ratio)
|
||||
}
|
||||
|
||||
/// Whether a command point keeps the elevation of its snap instead of being
|
||||
@@ -5695,7 +5704,8 @@ properties={:.1}ms picked={}",
|
||||
pub(super) fn on_viewport_scroll(&mut self, delta: mouse::ScrollDelta) -> Task<Message> {
|
||||
match scroll_intent(delta) {
|
||||
ScrollIntent::Zoom { notches } => {
|
||||
let mut s = notches * self.zoom_factor as f32 / 60.0;
|
||||
let mut s =
|
||||
notches * crate::app::settings::zoom_notch_steps(self.zoom_factor);
|
||||
if self.zoom_wheel_reversed {
|
||||
s = -s;
|
||||
}
|
||||
@@ -5753,9 +5763,9 @@ properties={:.1}ms picked={}",
|
||||
.record_nav_perf(crate::scene::NavPerfOp::Pan, started);
|
||||
}
|
||||
|
||||
/// Zoom the active view by `s`, in the camera's own units
|
||||
/// (`Camera::zoom`: `distance *= 1 - s / 10`). Shared by the wheel and
|
||||
/// the pinch.
|
||||
/// Zoom the active view by `s`, in the camera's own steps
|
||||
/// (`Camera::zoom`: `distance *= ZOOM_STEP.powf(s)`). Shared by the
|
||||
/// wheel and the pinch.
|
||||
fn zoom_view_at_cursor(&mut self, s: f32) -> Task<Message> {
|
||||
let nav_started = Instant::now();
|
||||
let i = self.active_tab;
|
||||
@@ -6717,6 +6727,7 @@ properties={:.1}ms picked={}",
|
||||
#[cfg(test)]
|
||||
mod scroll_intent_tests {
|
||||
use super::{pinch_zoom_steps, scroll_intent, ScrollIntent};
|
||||
use crate::scene::view::camera::Camera;
|
||||
use iced::mouse::ScrollDelta;
|
||||
|
||||
#[test]
|
||||
@@ -6740,13 +6751,46 @@ mod scroll_intent_tests {
|
||||
|
||||
/// A pinch is a scale factor, so the view has to end up exactly that much
|
||||
/// closer: fingers 5% apart divide the camera distance by 1.05.
|
||||
///
|
||||
/// Asked of the camera, not of the formula the step was derived from. The
|
||||
/// version that recomputed `1 - step / 10` here kept passing when the
|
||||
/// camera moved to an exponential zoom and the pinch quietly stopped
|
||||
/// being 1:1.
|
||||
#[test]
|
||||
fn pinch_is_one_to_one() {
|
||||
let zoom_after = 1.0 - pinch_zoom_steps(0.05) / 10.0;
|
||||
assert!((zoom_after - 1.0 / 1.05).abs() < 1e-6, "{zoom_after}");
|
||||
for magnification in [0.01_f32, 0.05, 0.2, 0.5, 1.0] {
|
||||
let mut camera = Camera::default();
|
||||
let before = camera.distance;
|
||||
camera.zoom(pinch_zoom_steps(magnification));
|
||||
let want = before / (1.0 + magnification);
|
||||
assert!(
|
||||
(camera.distance - want).abs() < 1e-4 * want,
|
||||
"m = {magnification}: {} wanted {want}",
|
||||
camera.distance
|
||||
);
|
||||
}
|
||||
assert!(pinch_zoom_steps(-0.05) < 0.0, "pinching in zooms out");
|
||||
// No pinch, no step: a zero delta must not divide the view away.
|
||||
assert_eq!(pinch_zoom_steps(0.0), 0.0);
|
||||
let mut camera = Camera::default();
|
||||
let before = camera.distance;
|
||||
camera.zoom(pinch_zoom_steps(0.0));
|
||||
assert_eq!(camera.distance, before);
|
||||
}
|
||||
|
||||
/// A gesture the platform should never send must not be able to throw the
|
||||
/// camera out of the world.
|
||||
#[test]
|
||||
fn a_degenerate_pinch_stays_finite() {
|
||||
for magnification in [-1.0_f32, -2.0, f32::NAN, f32::INFINITY] {
|
||||
let mut camera = Camera::default();
|
||||
camera.zoom(pinch_zoom_steps(magnification));
|
||||
assert!(
|
||||
camera.distance.is_finite() && camera.distance > 0.0,
|
||||
"m = {magnification} left distance at {}",
|
||||
camera.distance
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -330,6 +330,7 @@ impl OpenCADStudio {
|
||||
self.spacemouse_paused, self.spacemouse_details,
|
||||
),
|
||||
&self.snap_angle_input,
|
||||
&self.zoom_factor_input,
|
||||
{
|
||||
let header = self
|
||||
.tabs
|
||||
|
||||
Reference in new issue
Block a user