mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
* claim: #8993 * Reverse zoom direction no longer flips 3D mouse zoom (#8993) The checkbox sits under Wheel and trackpad and now reverses the wheel (and the navigation bridge's wheel-style steps) only. The 3D mouse cap zooms by its own per-axis invert switch alone: device_camera no longer takes a reverse_zoom argument, nor does the test model in Preferences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -851,17 +851,7 @@ pub(crate) fn device(app: &mut SolveApp, rect: Rect, caps: NavCaps) {
|
||||
}
|
||||
});
|
||||
if let Some(queued) = app.viewport.device_motion.take() {
|
||||
device_camera(
|
||||
&mut app.cam,
|
||||
queued.motion,
|
||||
queued.seconds,
|
||||
height,
|
||||
turn,
|
||||
app.preferences.zoom_reverse,
|
||||
app.preferences.free_orbit,
|
||||
anchor,
|
||||
pivot,
|
||||
);
|
||||
device_camera(&mut app.cam, queued.motion, queued.seconds, height, turn, app.preferences.free_orbit, anchor, pivot);
|
||||
}
|
||||
// A device app's motion (the navigation bridge), on the same terms.
|
||||
if let Some(queued) = app.viewport.bridge_motion.take() {
|
||||
@@ -920,13 +910,14 @@ pub(crate) fn bridge_camera(
|
||||
/// the frame rate. With `turn` off the view keeps its orientation. `anchor` is the model point the
|
||||
/// view holds while it zooms, as the wheel's cursor point (none zooms through the model, #7435).
|
||||
/// `pivot` is the point the view turns around and keeps still on screen (none: the view's centre).
|
||||
/// Reverse zoom direction is a wheel setting and leaves the cap alone: the cap's own direction is
|
||||
/// its per-axis invert, already in `motion` (#8993).
|
||||
pub(crate) fn device_camera(
|
||||
cam: &mut Camera,
|
||||
motion: Motion,
|
||||
seconds: f64,
|
||||
height: f64,
|
||||
turn: bool,
|
||||
reverse_zoom: bool,
|
||||
free_orbit: bool,
|
||||
anchor: Option<Vec3>,
|
||||
pivot: Option<Vec3>,
|
||||
@@ -939,8 +930,7 @@ pub(crate) fn device_camera(
|
||||
// Logical 3DxWare coordinates are X = left/right, Y = zoom and Z = up/down. The user's
|
||||
// profile can swap the physical forward/back and up/down axes before they reach here.
|
||||
cam.pan(x * height * 0.6 * seconds, z * height * 0.6 * seconds, height);
|
||||
let zoom_sign = if reverse_zoom { -1.0 } else { 1.0 };
|
||||
cam.zoom_at((-y * zoom_sign * 1.5 * seconds).exp(), anchor);
|
||||
cam.zoom_at((-y * 1.5 * seconds).exp(), anchor);
|
||||
if turn {
|
||||
// These are logical 3DxWare coordinates after the profile mapping: Rx is Tilt, Ry is
|
||||
// Roll and Rz is Spin. The KMJ profile swaps physical Ry/Rz for the selected navigation
|
||||
@@ -1710,7 +1700,7 @@ mod tests {
|
||||
}
|
||||
// Other presets keep the existing upright camera behavior.
|
||||
let mut cam = Camera { yaw: 0.0, pitch: 1.5, ..Default::default() };
|
||||
device_camera(&mut cam, Motion { translation: [0.0; 3], rotation: [1.0, 0.0, 0.0] }, 0.1, 600.0, true, false, false, None, None);
|
||||
device_camera(&mut cam, Motion { translation: [0.0; 3], rotation: [1.0, 0.0, 0.0] }, 0.1, 600.0, true, false, None, None);
|
||||
assert!(cam.pitch < std::f64::consts::FRAC_PI_2 && cam.pitch > 1.5);
|
||||
}
|
||||
|
||||
|
||||
@@ -2729,7 +2729,7 @@ fn space_mouse_rows(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
// The calibrated device moving as it does, with the test model beside it, then its dead zones.
|
||||
let (device, raw) = (app.space_mouse.device.clone(), app.space_mouse.raw.into());
|
||||
let (boat, motion, reverse) = (&mut app.space_mouse.boat, app.space_mouse.motion, (app.preferences.zoom_reverse, app.preferences.free_orbit));
|
||||
let (boat, motion, free_orbit) = (&mut app.space_mouse.boat, app.space_mouse.motion, app.preferences.free_orbit);
|
||||
if let Some(calibration) = device.as_ref().and_then(|device| app.preferences.space_mouse_calibrations.get_mut(device)) {
|
||||
ui_kit::divider(ui, "space_mouse_picture");
|
||||
// Under each circle's axes, what the view does with them: the rows' own labels.
|
||||
@@ -2739,11 +2739,11 @@ fn space_mouse_rows(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
crate::space_mouse_calibration::dead_zone_pads(ui, calibration, raw, false, &stories);
|
||||
ui_kit::text(ui, &help, ui_kit::Text::Hint("space_mouse_circles_help"));
|
||||
};
|
||||
picture_and_boat(ui, boat, motion, reverse, true, |ui| crate::space_mouse_model::live(ui, calibration, raw), pads);
|
||||
picture_and_boat(ui, boat, motion, free_orbit, true, |ui| crate::space_mouse_model::live(ui, calibration, raw), pads);
|
||||
} else if device.is_some() {
|
||||
// Not calibrated: the test model alone.
|
||||
ui_kit::divider(ui, "space_mouse_picture");
|
||||
picture_and_boat(ui, boat, motion, reverse, false, |_| {}, |_| {});
|
||||
picture_and_boat(ui, boat, motion, free_orbit, false, |_| {}, |_| {});
|
||||
}
|
||||
ui_kit::settings_form(ui, FORM_LABEL_WIDTH, |ui| {
|
||||
hint_under_controls(ui, crate::i18n::tr_label("Move and rotate the view together; sketches use pan and zoom only."), "space_mouse");
|
||||
@@ -2817,7 +2817,7 @@ fn picture_and_boat(
|
||||
ui: &mut egui::Ui,
|
||||
boat: &mut Option<crate::test_boat::Boat>,
|
||||
motion: Option<crate::space_mouse::DeviceMotion>,
|
||||
(reverse_zoom, free_orbit): (bool, bool),
|
||||
free_orbit: bool,
|
||||
has_picture: bool,
|
||||
picture: impl FnOnce(&mut egui::Ui),
|
||||
pads: impl FnOnce(&mut egui::Ui),
|
||||
@@ -2832,7 +2832,7 @@ fn picture_and_boat(
|
||||
let width = if beside { (room - PICTURE_WIDTH - gap).min(BOAT_MAX) } else { room.min(BOAT_MAX) };
|
||||
let mut boat_and_reset = |ui: &mut egui::Ui| {
|
||||
ui.vertical(|ui| {
|
||||
crate::test_boat::show(ui, boat, motion, reverse_zoom, free_orbit, egui::vec2(width.max(0.0), BOAT_HEIGHT));
|
||||
crate::test_boat::show(ui, boat, motion, free_orbit, egui::vec2(width.max(0.0), BOAT_HEIGHT));
|
||||
if ui_kit::button(ui, crate::i18n::tr("ui.3d_mouse_reset_view"), ui_kit::Button::Normal).clicked() {
|
||||
crate::test_boat::reset(boat);
|
||||
}
|
||||
|
||||
@@ -693,8 +693,8 @@ mod tests {
|
||||
|
||||
/// The camera math the page applies the motion with (`navigation::device`): `sketch` keeps
|
||||
/// the orientation, as editing a sketch does.
|
||||
fn navigate(cam: &mut Camera, motion: Motion, seconds: f64, height: f64, sketch: bool, reverse_zoom: bool) {
|
||||
crate::navigation::device_camera(cam, motion, seconds, height, !sketch, reverse_zoom, false, None, None);
|
||||
fn navigate(cam: &mut Camera, motion: Motion, seconds: f64, height: f64, sketch: bool) {
|
||||
crate::navigation::device_camera(cam, motion, seconds, height, !sketch, false, None, None);
|
||||
}
|
||||
|
||||
/// A window that has run a frame, with or without focus. A context that has never run one
|
||||
@@ -1174,14 +1174,14 @@ mod tests {
|
||||
fn six_axes_move_the_camera_and_sketches_keep_their_orientation() {
|
||||
let mut cam = Camera::default();
|
||||
let before = cam;
|
||||
navigate(&mut cam, raw(Axes { translation: [350, 350, 350], rotation: [350, 350, 350] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut cam, raw(Axes { translation: [350, 350, 350], rotation: [350, 350, 350] }), 0.02, 600.0, false);
|
||||
assert_ne!(cam.target, before.target);
|
||||
assert!(cam.distance < before.distance);
|
||||
assert_ne!(cam.yaw, before.yaw);
|
||||
assert_ne!(cam.pitch, before.pitch);
|
||||
assert_ne!(cam.roll, before.roll);
|
||||
let orientation = (cam.yaw, cam.pitch, cam.roll);
|
||||
navigate(&mut cam, raw(Axes { translation: [350, 0, 0], rotation: [350, 350, 350] }), 0.02, 600.0, true, false);
|
||||
navigate(&mut cam, raw(Axes { translation: [350, 0, 0], rotation: [350, 350, 350] }), 0.02, 600.0, true);
|
||||
assert_eq!((cam.yaw, cam.pitch, cam.roll), orientation);
|
||||
assert!(cam.is_valid());
|
||||
}
|
||||
@@ -1191,57 +1191,79 @@ mod tests {
|
||||
let before = Camera::default();
|
||||
|
||||
let mut slide = before;
|
||||
navigate(&mut slide, raw(Axes { translation: [350, 0, 0], rotation: [0; 3] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut slide, raw(Axes { translation: [350, 0, 0], rotation: [0; 3] }), 0.02, 600.0, false);
|
||||
assert_ne!(slide.target, before.target);
|
||||
assert_eq!(slide.distance, before.distance);
|
||||
|
||||
let mut push = before;
|
||||
navigate(&mut push, raw(Axes { translation: [0, 350, 0], rotation: [0; 3] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut push, raw(Axes { translation: [0, 350, 0], rotation: [0; 3] }), 0.02, 600.0, false);
|
||||
assert_eq!(push.target, before.target);
|
||||
assert!(push.distance < before.distance);
|
||||
|
||||
let mut lift = before;
|
||||
navigate(&mut lift, raw(Axes { translation: [0, 0, 350], rotation: [0; 3] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut lift, raw(Axes { translation: [0, 0, 350], rotation: [0; 3] }), 0.02, 600.0, false);
|
||||
assert_ne!(lift.target, before.target);
|
||||
assert_eq!(lift.distance, before.distance);
|
||||
|
||||
let mut tilt = before;
|
||||
navigate(&mut tilt, raw(Axes { translation: [0; 3], rotation: [350, 0, 0] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut tilt, raw(Axes { translation: [0; 3], rotation: [350, 0, 0] }), 0.02, 600.0, false);
|
||||
assert_eq!((tilt.yaw, tilt.roll), (before.yaw, before.roll));
|
||||
assert_ne!(tilt.pitch, before.pitch);
|
||||
|
||||
let mut roll = before;
|
||||
navigate(&mut roll, raw(Axes { translation: [0; 3], rotation: [0, 350, 0] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut roll, raw(Axes { translation: [0; 3], rotation: [0, 350, 0] }), 0.02, 600.0, false);
|
||||
assert_eq!((roll.pitch, roll.roll), (before.pitch, before.roll));
|
||||
assert_ne!(roll.yaw, before.yaw);
|
||||
|
||||
let mut twist = before;
|
||||
navigate(&mut twist, raw(Axes { translation: [0; 3], rotation: [0, 0, 350] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut twist, raw(Axes { translation: [0; 3], rotation: [0, 0, 350] }), 0.02, 600.0, false);
|
||||
assert_eq!((twist.yaw, twist.pitch), (before.yaw, before.pitch));
|
||||
assert_ne!(twist.roll, before.roll);
|
||||
}
|
||||
|
||||
/// Reverse zoom direction is a wheel and trackpad setting: it turns the wheel's zoom around
|
||||
/// and leaves the cap's, which has its own invert switch (#8993).
|
||||
#[test]
|
||||
fn reverse_zoom_setting_applies_to_spacemouse() {
|
||||
let before = Camera::default();
|
||||
let motion = raw(Axes { translation: [0, 350, 0], rotation: [0; 3] });
|
||||
let mut normal = before;
|
||||
navigate(&mut normal, motion, 0.02, 600.0, false, false);
|
||||
let mut reversed = before;
|
||||
navigate(&mut reversed, motion, 0.02, 600.0, false, true);
|
||||
assert!(normal.distance < before.distance);
|
||||
assert!(reversed.distance > before.distance);
|
||||
fn reverse_zoom_setting_flips_the_wheel_and_leaves_the_cap() {
|
||||
let wheel_zooms_in = |reverse: bool| {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.preferences.zoom_reverse = reverse;
|
||||
h.app.preferences.smooth_wheel_zoom = false;
|
||||
h.call("ui.move", serde_json::json!({"x":800,"y":500}));
|
||||
let before = h.app.cam.half_height();
|
||||
h.call("ui.scroll", serde_json::json!({"x":800,"y":500,"delta":120}));
|
||||
h.app.cam.half_height() < before
|
||||
};
|
||||
assert!(wheel_zooms_in(false));
|
||||
assert!(!wheel_zooms_in(true), "the wheel turns around");
|
||||
// The cap's zoom axis pushed forward with the wheel reversed or not, and with the cap's own
|
||||
// invert switch for that axis on or off.
|
||||
let cap_distance = |wheel_reversed: bool, cap_inverted: bool| {
|
||||
let mut app = app();
|
||||
app.preferences.zoom_reverse = wheel_reversed;
|
||||
app.preferences.space_mouse_reverse[1] = cap_inverted;
|
||||
let ctx = window(true);
|
||||
app.space_mouse.device = Some("hid 046d:c62b".into());
|
||||
let before = app.cam.distance;
|
||||
frame(&mut app, &ctx, DriverMapping::default(), Axes { translation: [0, 350, 0], rotation: [0; 3] }, 0.02);
|
||||
(before, app.cam.distance)
|
||||
};
|
||||
let (before, normal) = cap_distance(false, false);
|
||||
assert!(normal < before, "pushing the cap forward zooms in");
|
||||
assert_eq!(cap_distance(true, false).1, normal, "the cap zooms the same way with the wheel reversed");
|
||||
assert!(cap_distance(false, true).1 > before, "the cap's own invert switch turns it around");
|
||||
assert_eq!(cap_distance(true, true).1, cap_distance(false, true).1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_mode_tilts_roll_and_twists_spin() {
|
||||
let before = Camera::default();
|
||||
let mut tilt = before;
|
||||
navigate(&mut tilt, DriverMapping::object_mode().apply(Axes { translation: [0; 3], rotation: [0, 350, 0] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut tilt, DriverMapping::object_mode().apply(Axes { translation: [0; 3], rotation: [0, 350, 0] }), 0.02, 600.0, false);
|
||||
assert_eq!((tilt.yaw, tilt.pitch), (before.yaw, before.pitch));
|
||||
assert_ne!(tilt.roll, before.roll);
|
||||
let mut twist = before;
|
||||
navigate(&mut twist, DriverMapping::object_mode().apply(Axes { translation: [0; 3], rotation: [0, 0, 350] }), 0.02, 600.0, false, false);
|
||||
navigate(&mut twist, DriverMapping::object_mode().apply(Axes { translation: [0; 3], rotation: [0, 0, 350] }), 0.02, 600.0, false);
|
||||
assert_ne!(twist.yaw, before.yaw);
|
||||
assert_eq!((twist.pitch, twist.roll), (before.pitch, before.roll));
|
||||
// The physical tilt right (Ry−) rolls one way and the clockwise twist (Rz+) spins the
|
||||
|
||||
@@ -109,17 +109,8 @@ impl Boat {
|
||||
|
||||
/// `motion` for `seconds` on the boat's camera in a box `aspect` wide for its height of `height`
|
||||
/// points, with pan and zoom held so a sphere of `radius` stays inside it.
|
||||
pub(crate) fn step(
|
||||
cam: &mut Camera,
|
||||
motion: crate::space_mouse::Motion,
|
||||
seconds: f64,
|
||||
height: f64,
|
||||
radius: f64,
|
||||
aspect: f64,
|
||||
reverse_zoom: bool,
|
||||
free_orbit: bool,
|
||||
) {
|
||||
crate::navigation::device_camera(cam, motion, seconds, height, true, reverse_zoom, free_orbit, None, None);
|
||||
pub(crate) fn step(cam: &mut Camera, motion: crate::space_mouse::Motion, seconds: f64, height: f64, radius: f64, aspect: f64, free_orbit: bool) {
|
||||
crate::navigation::device_camera(cam, motion, seconds, height, true, free_orbit, None, None);
|
||||
cam.fov = FOV;
|
||||
// Zoom within limits: the nearest still shows the whole boat in the shorter side of the box.
|
||||
let shorter = aspect.min(1.0);
|
||||
@@ -171,14 +162,7 @@ pub(crate) fn project(model: &Model, cam: &Camera, rect: Rect) -> Frame {
|
||||
}
|
||||
|
||||
/// Draws the boat in a box of `size` and moves it by this frame's motion. `boat` is the page's state.
|
||||
pub(crate) fn show(
|
||||
ui: &mut egui::Ui,
|
||||
boat: &mut Option<Boat>,
|
||||
motion: Option<DeviceMotion>,
|
||||
reverse_zoom: bool,
|
||||
free_orbit: bool,
|
||||
size: egui::Vec2,
|
||||
) -> Option<Rect> {
|
||||
pub(crate) fn show(ui: &mut egui::Ui, boat: &mut Option<Boat>, motion: Option<DeviceMotion>, free_orbit: bool, size: egui::Vec2) -> Option<Rect> {
|
||||
let model = model()?;
|
||||
let t = Tokens::get();
|
||||
let now = ui.input(|i| i.time);
|
||||
@@ -188,7 +172,7 @@ pub(crate) fn show(
|
||||
state.seen = now;
|
||||
if let Some(motion) = motion {
|
||||
let aspect = f64::from(rect.width()) / f64::from(rect.height().max(1.0));
|
||||
step(&mut state.cam, motion.motion, motion.seconds, f64::from(rect.height()), model.radius, aspect, reverse_zoom, free_orbit);
|
||||
step(&mut state.cam, motion.motion, motion.seconds, f64::from(rect.height()), model.radius, aspect, free_orbit);
|
||||
}
|
||||
let painter = ui.painter_at(rect);
|
||||
painter.rect(rect, t.radius_sm, t.dialog_body, egui::Stroke::new(1.0, t.dialog_rule), egui::StrokeKind::Inside);
|
||||
|
||||
@@ -51,7 +51,7 @@ fn test_boat_step_orbits_pans_zooms_rolls_and_holds_the_boat_in_its_box() {
|
||||
let aspect = 1.4;
|
||||
let run = |m: Motion, seconds: f64| {
|
||||
let mut cam = start;
|
||||
step(&mut cam, m, seconds, 150.0, radius, aspect, false, false);
|
||||
step(&mut cam, m, seconds, 150.0, radius, aspect, false);
|
||||
cam
|
||||
};
|
||||
// Nothing moves nothing.
|
||||
@@ -70,17 +70,17 @@ fn test_boat_step_orbits_pans_zooms_rolls_and_holds_the_boat_in_its_box() {
|
||||
// Held down for a long time, the limits hold: nearest, farthest, and the pan room.
|
||||
let mut cam = start;
|
||||
for _ in 0..400 {
|
||||
step(&mut cam, motion([1.0, 1.0, 1.0], [0.0; 3]), 0.05, 150.0, radius, aspect, false, false);
|
||||
step(&mut cam, motion([1.0, 1.0, 1.0], [0.0; 3]), 0.05, 150.0, radius, aspect, false);
|
||||
}
|
||||
assert!(cam.distance >= radius * NEAREST / 1.0 - 1e-6 && cam.target.len() <= cam.distance * (FOV / 2.0).tan() + 1e-6);
|
||||
let mut cam = start;
|
||||
for _ in 0..400 {
|
||||
step(&mut cam, motion([-1.0, -1.0, -1.0], [0.0; 3]), 0.05, 150.0, radius, aspect, false, false);
|
||||
step(&mut cam, motion([-1.0, -1.0, -1.0], [0.0; 3]), 0.05, 150.0, radius, aspect, false);
|
||||
}
|
||||
assert!(cam.distance <= radius * FARTHEST + 1e-6);
|
||||
// Hostile numbers leave a valid camera.
|
||||
let mut cam = start;
|
||||
step(&mut cam, motion([f64::NAN, f64::INFINITY, 1e300], [f64::NAN; 3]), 0.05, 150.0, radius, aspect, false, false);
|
||||
step(&mut cam, motion([f64::NAN, f64::INFINITY, 1e300], [f64::NAN; 3]), 0.05, 150.0, radius, aspect, false);
|
||||
assert!(cam.is_valid());
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ fn test_boat_draws_inside_its_box_at_every_size_and_pose() {
|
||||
// Through the extremes of what the 3D mouse can do.
|
||||
for (translation, rotation) in [([0.0; 3], [0.0; 3]), ([1.0, -1.0, 1.0], [1.0, 0.0, 1.0]), ([-1.0, 1.0, -1.0], [0.0, 1.0, -1.0])] {
|
||||
for _ in 0..200 {
|
||||
step(&mut cam, motion(translation, rotation), 0.05, f64::from(h), m.radius, aspect, false, false);
|
||||
step(&mut cam, motion(translation, rotation), 0.05, f64::from(h), m.radius, aspect, false);
|
||||
}
|
||||
let frame = project(m, &cam, rect.shrink(1.0));
|
||||
assert!(!frame.indices.is_empty(), "{w}x{h}: something is drawn");
|
||||
@@ -141,7 +141,7 @@ fn test_boat_far_faces_come_first_and_a_page_opened_again_starts_afresh() {
|
||||
let moved = Some(DeviceMotion { motion: motion([0.0; 3], [1.0, 0.0, 0.0]), seconds: 0.05 });
|
||||
let draw = |time: f64, motion: Option<DeviceMotion>, state: &mut Option<Boat>| {
|
||||
let mut output = ctx.run_ui(egui::RawInput { time: Some(time), ..Default::default() }, |ui| {
|
||||
show(ui, state, motion, false, false, vec2(200.0, 150.0));
|
||||
show(ui, state, motion, false, vec2(200.0, 150.0));
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
};
|
||||
|
||||
+2
-1
@@ -19,7 +19,8 @@ versions. Two preferences opt into holding the model:
|
||||
While editing a sketch, the cap pans and zooms without changing the sketch's initial view. After you
|
||||
adjust the view with the navigation cube, the cap can rotate it too. The Navigation
|
||||
page in Preferences has an on/off switch, a speed control and, for each of the cap's six axes, an
|
||||
invert switch and a speed for the 3D mouse. The web app reads
|
||||
invert switch and a speed for the 3D mouse. The cap's zoom direction comes from its own invert
|
||||
switch; **Reverse zoom direction** under Wheel and trackpad reverses the wheel only (#8993). The web app reads
|
||||
it too, through the browser; Firefox needs a 3DxWare setting first (see
|
||||
[Getting started › 3D mouse in the browser](book/src/guide/getting-started.md#3d-mouse-in-the-browser)).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user