View cube TOP and BOTTOM turn the view as little as they can: a click kept forcing heading 0 (front edge at the bottom), so from the back-right iso view TOP spun the model round by 135°. The heading now goes to the nearest quarter turn; halfway between two (an iso view such as home) it takes the one nearer the standard plan view, so home → TOP still has the front at the bottom. The side faces are unchanged (the camera has no roll, so each has one upright view). Tests: nearest_quarter_turn for headings on, near and halfway between quarter turns, and the s39 scenario clicks TOP from the right side and keeps the right side at the bottom
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2fb3dcae6b
commit
c28e780f52
3 files changed
+57
-6
No files matched your search
@@ -2141,14 +2141,36 @@ fn view_cube(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
if let Some(dir) = clicked {
|
||||
let mut to = app.cam.looking_from(app.cam.from_frame(dir));
|
||||
// TOP and BOTTOM turn to the standard plan views (front edge at the bottom).
|
||||
// TOP and BOTTOM turn the view as little as they can: the heading to the nearest quarter
|
||||
// turn, so the cube lands square without spinning round.
|
||||
if dir.x == 0.0 && dir.y == 0.0 {
|
||||
to.yaw = 0.0;
|
||||
to.yaw = nearest_quarter_turn(app.cam.yaw);
|
||||
}
|
||||
app.animate_to(to);
|
||||
}
|
||||
}
|
||||
|
||||
/// The heading in whole quarter turns nearest `yaw` (radians). Halfway between two (an iso view,
|
||||
/// such as home), the one nearer heading 0, the standard plan view with the front edge at the
|
||||
/// bottom.
|
||||
fn nearest_quarter_turn(yaw: f64) -> f64 {
|
||||
if !yaw.is_finite() {
|
||||
return 0.0;
|
||||
}
|
||||
let q = std::f64::consts::FRAC_PI_2;
|
||||
let lo = (yaw / q).floor() * q;
|
||||
let hi = lo + q;
|
||||
let (dlo, dhi) = (yaw - lo, hi - yaw);
|
||||
if (dlo - dhi).abs() < 1e-6 {
|
||||
let off_front = |a: f64| ((a + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI).abs();
|
||||
if off_front(lo) <= off_front(hi) { lo } else { hi }
|
||||
} else if dlo < dhi {
|
||||
lo
|
||||
} else {
|
||||
hi
|
||||
}
|
||||
}
|
||||
|
||||
/// How far along each edge a view cube corner is cut off (the cube's faces are at ±1).
|
||||
const CUBE_CHAMFER: f64 = 0.38;
|
||||
|
||||
@@ -2257,8 +2279,9 @@ fn cube_to_screen((r, u): (Vec3, Vec3), c: Pos2, p: Vec3) -> Pos2 {
|
||||
pos2(c.x + (p.dot(r) * CUBE_SCALE) as f32, c.y - (p.dot(u) * CUBE_SCALE) as f32)
|
||||
}
|
||||
|
||||
/// A face's text directions (right, up): read from outside the cube, and upright in the view
|
||||
/// clicking the face turns to (TOP and BOTTOM: the plan views, front edge at the bottom).
|
||||
/// A face's text directions (right, up): read from outside the cube, and upright in the face's
|
||||
/// standard view (TOP and BOTTOM: the plan views with the front edge at the bottom; clicked from
|
||||
/// another heading, they turn with the cube).
|
||||
fn cube_label_axes(n: Vec3) -> (Vec3, Vec3) {
|
||||
if n.z.abs() > 0.5 { (Vec3::X, Vec3::Y * n.z.signum()) } else { (Vec3::Z.cross(n), Vec3::Z) }
|
||||
}
|
||||
@@ -2909,7 +2932,7 @@ mod tests {
|
||||
#[test]
|
||||
fn view_cube_labels_read_upright_in_their_own_view() {
|
||||
for tg in cube_targets().iter().filter(|t| t.kind == CubeKind::Face) {
|
||||
// The view clicking the face turns to (TOP and BOTTOM with yaw 0).
|
||||
// The face's standard view (TOP and BOTTOM with yaw 0).
|
||||
let mut cam = solvecraft_engine::render::Camera::default().looking_from(tg.dir);
|
||||
if tg.dir.z.abs() > 0.5 {
|
||||
cam.yaw = 0.0;
|
||||
@@ -2947,6 +2970,28 @@ mod tests {
|
||||
assert!(worst <= 0.5, "a label vertex {worst} px outside its face");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_cube_top_turns_to_the_nearest_quarter_turn() {
|
||||
// (current heading, heading after TOP or BOTTOM), degrees.
|
||||
for (from, to) in [
|
||||
(0.0, 0.0),
|
||||
(30.0, 0.0),
|
||||
(-80.0, -90.0),
|
||||
(100.0, 90.0),
|
||||
(170.0, 180.0),
|
||||
(400.0, 360.0),
|
||||
// Halfway (iso views): toward the standard plan view.
|
||||
(-45.0, 0.0),
|
||||
(45.0, 0.0),
|
||||
(135.0, 90.0),
|
||||
(-135.0, -90.0),
|
||||
] {
|
||||
let got = nearest_quarter_turn(f64::to_radians(from)).to_degrees();
|
||||
assert!((got - to).abs() < 1e-9, "{from}° → {got}°, expected {to}°");
|
||||
}
|
||||
assert_eq!(nearest_quarter_turn(f64::NAN), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_cube_edges_are_targets() {
|
||||
let targets = cube_targets();
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
{"expect": {"camera": {"yaw_deg": 0, "pitch_deg": 45}}},
|
||||
{"click": {"handle": "cube:front-right"}},
|
||||
{"expect": {"camera": {"yaw_deg": -45, "pitch_deg": 0}}},
|
||||
{"note": "TOP turns as little as it can: from the right side it keeps the heading, so RIGHT stays at the bottom instead of the view spinning round to FRONT."},
|
||||
{"click": {"handle": "cube:top-right"}},
|
||||
{"expect": {"camera": {"yaw_deg": -90, "pitch_deg": 45}}},
|
||||
{"click": {"handle": "cube:top"}},
|
||||
{"expect": {"camera": {"yaw_deg": -90, "pitch_deg": 90}}},
|
||||
{"call": "ui.view", "params": {"view": "home"}},
|
||||
{"click": {"handle": "cube:top-back-right"}},
|
||||
{"expect": {"camera": {"yaw_deg": -135, "pitch_deg": 35.26}}},
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
- **Viewport.** Click to select, Shift or Ctrl to add, drag for a window or crossing box. The
|
||||
wheel zooms at the cursor, the middle button pans and Shift+middle orbits (the mouse preset in
|
||||
Preferences switches to SolidWorks or Inventor style). The view cube and the navigation bar
|
||||
change the view: a cube face turns to its standard view, an edge to the view halfway between
|
||||
change the view: a cube face turns to look straight at it (TOP and BOTTOM keep the current
|
||||
heading, squared to the nearest quarter turn), an edge to the view halfway between
|
||||
its two faces, a chamfered corner to that corner's iso view, and the house goes home. Right-click for the marking menu: eight commands around the cursor, plus a
|
||||
menu for whatever is under it.
|
||||
- **Dialogs** dock on the right with a live preview. Most features also show drag handles and a
|
||||
|
||||
Reference in new issue
Block a user