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:
N3MI-DGandClaude Opus 5.5 committed 2026-10-11 09:18:29 +11:00
1 parent 2fb3dcae6b
commit c28e780f52
3 files changed
+57 -6

No files matched your search

+50 -5
View File
@@ -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}}},
+2 -1
View File
@@ -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