correction orbital camera inputs
This commit is contained in:
@@ -2,6 +2,18 @@
|
||||
|
||||
> Étape 16 (Phase 5 — Documentation & Polish) **terminée** le 2026-07-19.
|
||||
>
|
||||
> **Tuning caméra 2026-07-19** — sur rétroaction utilisateur (caméra trop sensible, orbit permanent) :
|
||||
> (1) `CameraController` a gagné deux champs configurables `orbit_sensitivity`/`zoom_factor` (defaults : 0.01→0.005
|
||||
> rad/px, 0.9/tic) ; (2) `InputState` normalise `PixelDelta`/32 en unités « notch » (le Wayland renvoyait ~100 px/tic,
|
||||
> donc `0.9^100` → zoom au clamping en un geste) ; (3) le `demo` orbite désormais **sur clic gauche enfoncé** (arc-rotate
|
||||
> classique). Docs `camera-input.md`/`examples.md` mises à jour. 51 tests OK.
|
||||
>
|
||||
> **Bug fix 2026-07-19** — `InputState::begin_frame()` remettait à zéro `mouse_delta`/`scroll` AVANT que
|
||||
> `update()` ne les lise, alors que les événements winit (CursorMoved/MouseWheel) s'accumulent ENTRE deux
|
||||
> frames. Résultat : la caméra orbitale du `demo` ne bougeait jamais (souris/molette toujours (0,0) dans
|
||||
> `update`). Corrigé par rotation des accumulateurs (`frame_mouse_delta`/`frame_scroll` → queryables à
|
||||
> `begin_frame`), aligné sur le pattern clavier/boutons. Tests mis à jour (ordre réel winit) + 45/50 tests OK.
|
||||
>
|
||||
> Traduction anglaise de toute la documentation (hors `docs/tech/`, DRAFT/PLAN/ROADMAP) **terminée** le 2026-07-19 :
|
||||
> `docs/user/*`, `README.md`, READMEs de modules, doc/rustdoc de tous les `.rs` (src + examples + tests),
|
||||
> `Étape`→`Step` global. Vérifications : 50 tests OK, `cargo fmt` clean, aucun lien cassé, 0 accent restant hors zone franche.
|
||||
|
||||
@@ -49,6 +49,13 @@ ctrl.apply_to(app.scene.camera_mut()); // write the framing into the active cam
|
||||
`CameraController::from_camera(&cam)` rebuilds a controller from an existing camera
|
||||
(useful to start the orbit from a manual framing).
|
||||
|
||||
Two public fields tune the feel of the camera (defaults in parentheses):
|
||||
|
||||
| Field | Meaning | Default |
|
||||
|-------|---------|---------|
|
||||
| `orbit_sensitivity` | radians of yaw per pixel of mouse delta | `0.005` (~110° per full window width) |
|
||||
| `zoom_factor` | multiplicative distance change per wheel notch (`distance *= factor^scroll`) | `0.9` (10% per notch) |
|
||||
|
||||
The exact wiring snippet (orbit + zoom + reset + `1`/`2`/`3` presets, driven from
|
||||
`app.input`) is in [`demo.rs`](../../lib/examples/demo.rs), `update()` section.
|
||||
|
||||
@@ -64,7 +71,13 @@ every frame (`begin_frame`/`end_frame` around your `update`). Three semantics pe
|
||||
| **released** | `key_released(code)`, `mouse_button_released(btn)` | true **only** on the release frame |
|
||||
|
||||
Plus: `mouse_position() -> (f32, f32)`, `mouse_delta() -> (f32, f32)` (accumulated over the
|
||||
frame, reset between frames), `scroll_delta() -> (f32, f32)` (wheel).
|
||||
frame, reset between frames), `scroll_delta() -> (f32, f32)` (wheel, in **line/notch units** —
|
||||
`PixelDelta` events are normalized by /32 so one physical wheel notch ≈ 1.0 on every backend).
|
||||
|
||||
> **Button-gated orbit**: `mouse_delta()` returns movement *whenever* the mouse moves. For a
|
||||
> classic arc-rotate camera, apply it only while a button is held — that is what the `demo` does:
|
||||
> `if app.input.mouse_button_held(MouseButton::Left) { self.camera.orbit(dx, dy); }`.
|
||||
> Free-movement orbit (no button) is also possible, just drop the condition.
|
||||
|
||||
`KeyCode` values are winit's physical codes (`winit::keyboard::KeyCode`); mouse buttons are
|
||||
`winit::event::MouseButton`. The library does not re-export them: if your code mentions
|
||||
@@ -73,12 +86,15 @@ applications (like `simple`/`cube`) don't need winit: `app.input` remains usable
|
||||
`KeyCode` comparisons require the import.
|
||||
|
||||
```rust
|
||||
use winit::event::MouseButton;
|
||||
use winit::keyboard::KeyCode;
|
||||
|
||||
fn update(&mut self, app: &mut wsg_lib::App) {
|
||||
// Orbit + zoom driven by the mouse (excerpts from demo):
|
||||
// Orbit (left-drag gated) + zoom driven by the mouse (excerpts from demo):
|
||||
let (dx, dy) = app.input.mouse_delta();
|
||||
if app.input.mouse_button_held(MouseButton::Left) {
|
||||
self.camera.orbit(dx, dy);
|
||||
}
|
||||
let (_, sy) = app.input.scroll_delta();
|
||||
self.camera.zoom(sy);
|
||||
|
||||
@@ -103,6 +119,7 @@ fn update(&mut self, app: &mut wsg_lib::App) {
|
||||
| FPS camera (WASD) | `key_held(KeyCode::KeyW)` in `update` → move `camera.position`/`target`; override `render()` if needed |
|
||||
| Changing the orbit target | `ctrl.target = subject_position;` (following an object) |
|
||||
| View presets | `key_pressed(Digit1/2/3)` → write yaw/pitch/distance (from the `demo`) |
|
||||
| Tuning the camera speed | `ctrl.orbit_sensitivity = 0.003;` (slower orbit), `ctrl.zoom_factor = 0.95;` (gentler zoom) |
|
||||
|
||||
## Links
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Seven examples live in [`lib/examples/`](../../lib/examples/) and all launch wit
|
||||
|---------|----------|---------------|--------------------|
|
||||
| `simple` | `cargo run -p wsg-lib --example simple` | The minimal declarative workflow: a two-tone 2D quad, **unlit**, rendered automatically. The "15 lines, no wgpu" model | [Quickstart](quickstart.md), [Materials](materials.md) (§ unlit) |
|
||||
| `cube` | `cargo run -p wsg-lib --example cube` | The 3D MVP: a textured (checkerboard) cube, lit (directional + point + spot), spinning | [Meshes](meshes.md), [Materials](materials.md), [Lights](lights.md) |
|
||||
| `demo` | `cargo run -p wsg-lib --example demo` | The full showcase: ground + 6 primitives, textures, 3 lights, **shadows**, **orbital camera** on keyboard/mouse (drag = orbit, wheel = zoom, `R` = reset, `1`/`2`/`3` = presets) | [All pages](README.md) |
|
||||
| `demo` | `cargo run -p wsg-lib --example demo` | The full showcase: ground + 6 primitives, textures, 3 lights, **shadows**, **orbital camera** on keyboard/mouse (left-drag = orbit, wheel = zoom, `R` = reset, `1`/`2`/`3` = presets) | [All pages](README.md) |
|
||||
| `shadow_test` | `cargo run -p wsg-lib --example shadow_test` | Isolated shadow mapping: a cube casts a PCF-softened shadow on the ground (`clear_lights` technique → caster at index 0) | [Shadows](shadows.md) |
|
||||
| `spot_test` | `cargo run -p wsg-lib --example spot_test` | Isolated spot (ambient nearly zero): the directed beam, the penumbra, the attenuation | [Lights](lights.md) |
|
||||
| `manual` | `cargo run -p wsg-lib --example manual` | The **advanced** workflow: `Context`/`Renderer`/`PipelineCache` driven by hand, without the `App` facade (winit 0.30 `ApplicationHandler`) | below |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! * a **procedural texture** per mesh (checker / stripe grids, no assets on disk),
|
||||
//! * the **standard** Phong material wired to those textures,
|
||||
//! * an **orbital camera** driven live by the unified input state (Step 15.B):
|
||||
//! moving the mouse orbits (yaw/pitch), the wheel zooms (distance),
|
||||
//! hold the **left mouse button** and drag to orbit (yaw/pitch), the wheel zooms (distance),
|
||||
//! * `R` resets the view, keys `1`/`2`/`3` jump to front / side / top presets,
|
||||
//! * a **directional** light (the shadow caster) + a **point** light + a **spot** light,
|
||||
//! so the shadow of the cube and the colored light halos are all visible,
|
||||
@@ -19,6 +19,7 @@
|
||||
//! `cargo run -p wsg-lib --example demo`
|
||||
|
||||
use glam::{Quat, Vec3};
|
||||
use winit::event::MouseButton;
|
||||
use winit::keyboard::KeyCode;
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::app::AppBuilder;
|
||||
@@ -177,9 +178,13 @@ impl AppHandler for Demo {
|
||||
|
||||
fn update(&mut self, app: &mut wsg_lib::App) {
|
||||
// ---- Orbital camera from unified input ----
|
||||
// Moving the mouse orbits (yaw/pitch); the wheel zooms (distance).
|
||||
// Classic arc-rotate: orbit ONLY while the left button is held (drag); the wheel zooms
|
||||
// without any button. Sensitivities use the library defaults (0.005 rad/px orbit, 0.9x
|
||||
// per wheel notch); tune them via `camera.orbit_sensitivity` / `camera.zoom_factor`.
|
||||
let (dx, dy) = app.input.mouse_delta();
|
||||
if app.input.mouse_button_held(MouseButton::Left) {
|
||||
self.camera.orbit(dx, dy);
|
||||
}
|
||||
let (_, sy) = app.input.scroll_delta();
|
||||
self.camera.zoom(sy);
|
||||
|
||||
|
||||
+55
-22
@@ -53,7 +53,10 @@ pub struct InputState {
|
||||
mouse_position: (f32, f32),
|
||||
/// Previous absolute position, to derive the `CursorMoved` delta.
|
||||
last_mouse_position: Option<(f32, f32)>,
|
||||
/// Cumulative relative movement during the current frame.
|
||||
/// Frame accumulator for the relative movement (events between two `begin_frame` calls),
|
||||
/// rotated into `mouse_delta` at the next `begin_frame` (same pattern as the keyboard).
|
||||
frame_mouse_delta: (f32, f32),
|
||||
/// Cumulative relative movement during the current frame (queryable in `update`).
|
||||
mouse_delta: (f32, f32),
|
||||
/// Buttons currently held down.
|
||||
held_buttons: HashSet<MouseButton>,
|
||||
@@ -66,7 +69,10 @@ pub struct InputState {
|
||||
frame_released_buttons: HashSet<MouseButton>,
|
||||
|
||||
// ---- Wheel ----
|
||||
/// Cumulative scroll during the current frame (x, y).
|
||||
/// Frame accumulator for the scroll (x, y) (events between two `begin_frame` calls),
|
||||
/// rotated into `scroll` at the next `begin_frame`.
|
||||
frame_scroll: (f32, f32),
|
||||
/// Cumulative scroll during the current frame (x, y) (queryable in `update`).
|
||||
scroll: (f32, f32),
|
||||
// ---- Gamepad (reserved) ----
|
||||
// (DRAFT D7: optional minimal v1, deferred — the API will extend without breakage.)
|
||||
@@ -95,7 +101,11 @@ impl InputState {
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => self.wheel(*x, *y),
|
||||
MouseScrollDelta::PixelDelta(p) => self.wheel(p.x as f32, p.y as f32),
|
||||
// PixelDelta (most Wayland compositors) reports raw pixels — one wheel notch is
|
||||
// typically ~32 px, so normalize to line (notch) units to keep `scroll_delta()`
|
||||
// in the same scale as LineDelta backends (X11). Without this, `zoom()` would
|
||||
// apply `factor^100` per notch and snap to the clamp in a single wheel step.
|
||||
MouseScrollDelta::PixelDelta(p) => self.wheel(p.x as f32 / 32.0, p.y as f32 / 32.0),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
@@ -130,43 +140,50 @@ impl InputState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the cursor position and accumulates the relative movement. Called by
|
||||
/// [`InputState::handle_window_event`].
|
||||
/// Updates the cursor position and accumulates the relative movement in the frame buffer. Called
|
||||
/// by [`InputState::handle_window_event`]; the frame buffer is rotated into the queryable
|
||||
/// `mouse_delta` at the next [`InputState::begin_frame`].
|
||||
fn cursor_move(&mut self, x: f32, y: f32) {
|
||||
if let Some((px, py)) = self.last_mouse_position {
|
||||
self.mouse_delta.0 += x - px;
|
||||
self.mouse_delta.1 += y - py;
|
||||
self.frame_mouse_delta.0 += x - px;
|
||||
self.frame_mouse_delta.1 += y - py;
|
||||
}
|
||||
self.last_mouse_position = Some((x, y));
|
||||
self.mouse_position = (x, y);
|
||||
}
|
||||
|
||||
/// Accumulates the wheel scroll. Called by [`InputState::handle_window_event`].
|
||||
/// Accumulates the wheel scroll in the frame buffer (in **line/notch units** — `handle_window_event`
|
||||
/// normalizes `PixelDelta` by /32 before calling this). The frame buffer is rotated into the
|
||||
/// queryable `scroll` at the next [`InputState::begin_frame`].
|
||||
fn wheel(&mut self, dx: f32, dy: f32) {
|
||||
self.scroll.0 += dx;
|
||||
self.scroll.1 += dy;
|
||||
self.frame_scroll.0 += dx;
|
||||
self.frame_scroll.1 += dy;
|
||||
}
|
||||
|
||||
/// Starts a new input frame: **rotates** the event accumulators
|
||||
/// (accumulated between two `begin_frame` calls) into the queryable sets `pressed`/`released`, and
|
||||
/// zeroes the mouse delta and the wheel. Call this **before** `AppHandler::update`.
|
||||
/// Starts a new input frame: **rotates** all the event accumulators (keyboard pressed/released,
|
||||
/// buttons, mouse delta and wheel — accumulated between two `begin_frame` calls) into the
|
||||
/// queryable state. Call this **before** `AppHandler::update`.
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.pressed = std::mem::take(&mut self.frame_pressed);
|
||||
self.released = std::mem::take(&mut self.frame_released);
|
||||
self.pressed_buttons = std::mem::take(&mut self.frame_pressed_buttons);
|
||||
self.released_buttons = std::mem::take(&mut self.frame_released_buttons);
|
||||
self.mouse_delta = (0.0, 0.0);
|
||||
self.scroll = (0.0, 0.0);
|
||||
self.mouse_delta = self.frame_mouse_delta;
|
||||
self.frame_mouse_delta = (0.0, 0.0);
|
||||
self.scroll = self.frame_scroll;
|
||||
self.frame_scroll = (0.0, 0.0);
|
||||
}
|
||||
|
||||
/// Ends a frame: clears the transient sets `pressed`/`released` (already consumed by
|
||||
/// `update`). The `held` states and the position are kept. Call this **after**
|
||||
/// `AppHandler::update` (or `render`).
|
||||
/// Ends a frame: clears the transient state consumed by `update` (`pressed`/`released`, button
|
||||
/// sets, queryable mouse delta and wheel). The `held` states and the cursor position are kept.
|
||||
/// Call this **after** `AppHandler::update` (or `render`).
|
||||
pub fn end_frame(&mut self) {
|
||||
self.pressed.clear();
|
||||
self.released.clear();
|
||||
self.pressed_buttons.clear();
|
||||
self.released_buttons.clear();
|
||||
self.mouse_delta = (0.0, 0.0);
|
||||
self.scroll = (0.0, 0.0);
|
||||
}
|
||||
|
||||
// ---- Keyboard queries ----
|
||||
@@ -194,7 +211,9 @@ impl InputState {
|
||||
pub fn mouse_delta(&self) -> (f32, f32) {
|
||||
self.mouse_delta
|
||||
}
|
||||
/// Cumulative wheel scroll during the current frame (`(dx, dy)`, `dy > 0` = upward).
|
||||
/// Cumulative wheel scroll during the current frame, in **line (notch) units**
|
||||
/// (`(dx, dy)`, `dy > 0` = wheel up). `PixelDelta` events are normalized by /32 so the scale
|
||||
/// is backend-independent (one physical wheel notch ≈ 1.0).
|
||||
pub fn scroll_delta(&self) -> (f32, f32) {
|
||||
self.scroll
|
||||
}
|
||||
@@ -260,30 +279,44 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mouse_delta_and_position_accumulate() {
|
||||
// Real winit order: events arrive BETWEEN two frames, then `begin_frame` rotates the
|
||||
// accumulator into the queryable delta (a `begin_frame` before the events would not lose
|
||||
// them, the queryable copy is separate from the frame buffer).
|
||||
let mut input = InputState::new();
|
||||
input.begin_frame();
|
||||
input.begin_frame(); // frame 1 starts (empty)
|
||||
input.cursor_move(10.0, 20.0);
|
||||
input.cursor_move(30.0, 40.0);
|
||||
input.end_frame();
|
||||
|
||||
// Frame 2 starts: the movement accumulated during frame 1 is rotated into the queryable
|
||||
// delta and read by `update`.
|
||||
input.begin_frame();
|
||||
assert_eq!(input.mouse_delta(), (20.0, 20.0));
|
||||
assert_eq!(input.mouse_position(), (30.0, 40.0));
|
||||
input.end_frame();
|
||||
|
||||
// New frame: the delta is reset to zero at the start, the position persists.
|
||||
// Frame 3 without new events: the queryable delta is back to zero, the position persists.
|
||||
input.begin_frame();
|
||||
assert_eq!(input.mouse_delta(), (0.0, 0.0));
|
||||
assert_eq!(input.mouse_position(), (30.0, 40.0));
|
||||
|
||||
// Frame 4: a small movement accumulates and is rotated in again.
|
||||
input.cursor_move(31.0, 42.0);
|
||||
input.begin_frame();
|
||||
assert_eq!(input.mouse_delta(), (1.0, 2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_accumulates_per_frame() {
|
||||
// Real winit order: wheel events accumulate between frames, `begin_frame` rotates them.
|
||||
let mut input = InputState::new();
|
||||
input.begin_frame();
|
||||
input.wheel(1.0, 2.0);
|
||||
input.wheel(0.5, -1.0);
|
||||
input.begin_frame();
|
||||
assert_eq!(input.scroll_delta(), (1.5, 1.0));
|
||||
input.end_frame();
|
||||
|
||||
// Next frame without new scroll: the queryable delta is zero.
|
||||
input.begin_frame();
|
||||
assert_eq!(input.scroll_delta(), (0.0, 0.0));
|
||||
}
|
||||
|
||||
@@ -130,6 +130,10 @@ pub struct CameraController {
|
||||
pub distance: f32,
|
||||
/// World-space point the camera looks at and orbits around.
|
||||
pub target: Vec3,
|
||||
/// Orbit sensitivity (radians of yaw per pixel of mouse delta); see [`DEFAULT_ORBIT_SENSITIVITY`].
|
||||
pub orbit_sensitivity: f32,
|
||||
/// Multiplicative zoom factor applied per unit of vertical scroll; see [`DEFAULT_ZOOM_FACTOR`].
|
||||
pub zoom_factor: f32,
|
||||
}
|
||||
|
||||
impl Default for CameraController {
|
||||
@@ -139,13 +143,17 @@ impl Default for CameraController {
|
||||
pitch: 0.0,
|
||||
distance: 3.0,
|
||||
target: Vec3::ZERO,
|
||||
orbit_sensitivity: DEFAULT_ORBIT_SENSITIVITY,
|
||||
zoom_factor: DEFAULT_ZOOM_FACTOR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sensitivity of the orbit drag (radians of yaw per pixel of horizontal mouse delta).
|
||||
pub const DEFAULT_ORBIT_SENSITIVITY: f32 = 0.01;
|
||||
/// Multiplicative zoom factor applied per unit of vertical scroll.
|
||||
/// 0.005 gives ~110° per full window width — a comfortable default; raise it for smaller viewports.
|
||||
pub const DEFAULT_ORBIT_SENSITIVITY: f32 = 0.005;
|
||||
/// Multiplicative zoom factor applied per unit of vertical scroll (one wheel notch ≈ 1 unit after
|
||||
/// `InputState` normalization). 0.9 → 10% distance change per notch.
|
||||
pub const DEFAULT_ZOOM_FACTOR: f32 = 0.9;
|
||||
|
||||
impl CameraController {
|
||||
@@ -164,6 +172,8 @@ impl CameraController {
|
||||
pitch: pitch.clamp(-PITCH_LIMIT, PITCH_LIMIT),
|
||||
distance,
|
||||
target: camera.target,
|
||||
orbit_sensitivity: DEFAULT_ORBIT_SENSITIVITY,
|
||||
zoom_factor: DEFAULT_ZOOM_FACTOR,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,19 +193,21 @@ impl CameraController {
|
||||
}
|
||||
|
||||
/// Applies an orbit drag (mouse delta in pixels): `dx` rotates yaw, `dy` rotates pitch
|
||||
/// (inverted so dragging up tilts the view up). Pitch is clamped to ±[`PITCH_LIMIT`].
|
||||
/// (inverted so dragging up tilts the view up). Pitch is clamped to ±[`PITCH_LIMIT`]. The
|
||||
/// rotation speed is scaled by `self.orbit_sensitivity`.
|
||||
pub fn orbit(&mut self, dx: f32, dy: f32) {
|
||||
self.yaw -= dx * DEFAULT_ORBIT_SENSITIVITY;
|
||||
self.pitch = (self.pitch + dy * DEFAULT_ORBIT_SENSITIVITY).clamp(-PITCH_LIMIT, PITCH_LIMIT);
|
||||
self.yaw -= dx * self.orbit_sensitivity;
|
||||
self.pitch = (self.pitch + dy * self.orbit_sensitivity).clamp(-PITCH_LIMIT, PITCH_LIMIT);
|
||||
}
|
||||
|
||||
/// Zooms in/out by an exponential factor on the vertical wheel scroll (`scroll_y`): positive
|
||||
/// scroll zooms in (distance shrinks). Clamped to a sane `[0.1, 100]` range.
|
||||
/// Zooms in/out by an exponential factor on the vertical wheel scroll (`scroll_y`, in wheel
|
||||
/// notches): positive scroll zooms in (distance shrinks). Clamped to a sane `[0.1, 100]` range.
|
||||
/// The per-notch factor is `self.zoom_factor`.
|
||||
pub fn zoom(&mut self, scroll_y: f32) {
|
||||
if scroll_y == 0.0 {
|
||||
return;
|
||||
}
|
||||
let factor = DEFAULT_ZOOM_FACTOR.powf(scroll_y);
|
||||
let factor = self.zoom_factor.powf(scroll_y);
|
||||
self.distance = (self.distance * factor).clamp(0.1, 100.0);
|
||||
}
|
||||
|
||||
@@ -254,6 +266,28 @@ mod tests {
|
||||
assert!((back.position - cam.position).length() < 1e-2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sensitivity_is_configurable() {
|
||||
let mut slow = CameraController::default();
|
||||
let mut fast = CameraController::default();
|
||||
slow.orbit_sensitivity = 0.001; // one fifth of the default
|
||||
fast.orbit_sensitivity = 0.02; // four times the default
|
||||
slow.orbit(100.0, 0.0);
|
||||
fast.orbit(100.0, 0.0);
|
||||
assert!(
|
||||
(slow.yaw - fast.yaw).abs() > 1.0,
|
||||
"faster sensitivity must rotate more"
|
||||
);
|
||||
// Zoom factor: a gentler factor moves the distance less for the same scroll.
|
||||
let mut gentle = CameraController::default();
|
||||
gentle.zoom_factor = 0.99;
|
||||
let mut aggressive = CameraController::default();
|
||||
aggressive.zoom_factor = 0.8;
|
||||
gentle.zoom(3.0);
|
||||
aggressive.zoom(3.0);
|
||||
assert!(gentle.distance > aggressive.distance);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_restores_defaults() {
|
||||
let mut ctrl = CameraController::default();
|
||||
|
||||
Reference in New Issue
Block a user