View cube with Y up (#3): the cube is drawn and clicked in the camera's up-axis frame, so TOP is the world's up; TOP and BOTTOM turn to the standard plan views (yaw 0). Scenario view_cube_y_up; the camera check can test the world direction the camera looks from

Refs #15, #3

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-10 06:55:10 +00:00
1 parent d1cdc4d061
commit 9dd901649c
3 files changed
+44 -6

No files matched your search

+10 -1
View File
@@ -577,7 +577,8 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
}
}
"camera" => {
// {"camera": {"yaw_deg": y, "pitch_deg": p}}: where the camera looks from (±0.5°),
// {"camera": {"yaw_deg": y, "pitch_deg": p, "back"?: [x,y,z]}}: where the camera
// looks from (yaw and pitch in its up-axis frame, ±0.5°),
// once any view animation has finished.
for _ in 0..240 {
if h.app.cam_anim.is_none() {
@@ -590,6 +591,14 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
if !near(yaw, &v["yaw_deg"]) || !near(pitch, &v["pitch_deg"]) {
return Err(format!("camera yaw {yaw:.2}°, pitch {pitch:.2}°, want {v}"));
}
// "back": the world direction from the target to the eye.
if let Some(want) = v["back"].as_array() {
let b = h.app.cam.back();
let w = |i: usize| want.get(i).and_then(Value::as_f64).unwrap_or(f64::NAN);
if (b.x - w(0)).abs() > 1e-3 || (b.y - w(1)).abs() > 1e-3 || (b.z - w(2)).abs() > 1e-3 {
return Err(format!("camera looks from {b:?}, want {want:?}"));
}
}
}
"count" => {
// {"count": {"name": n, "value": x}}: a published count.
+18 -5
View File
@@ -1879,7 +1879,7 @@ fn cpu_render(app: &mut SolveApp, ctx: &egui::Context, painter: &egui::Painter,
/// Where a point in view-cube units (faces at ±1) is drawn.
pub(crate) fn cube_point(app: &SolveApp, rect: Rect, p: Vec3) -> Pos2 {
let (r, u, _) = app.cam.basis();
let (r, u, _) = cube_basis(&app.cam);
pos2(rect.right() - 80.0 + (p.dot(r) * CUBE_SCALE) as f32, rect.top() + 80.0 - (p.dot(u) * CUBE_SCALE) as f32)
}
@@ -1890,20 +1890,29 @@ const CUBE_SCALE: f64 = 32.0;
/// (orthographic views) and the 8 corner facets (iso views); the edges between them are not
/// targets. Hover lights the face or facet under the pointer, a click turns the view to it, the
/// house goes home.
/// The camera's basis in its up-axis frame (Z up): the cube's TOP is the world's up, X, Y or Z.
fn cube_basis(cam: &Camera) -> (Vec3, Vec3, Vec3) {
let (r, u, b) = cam.basis();
(cam.to_frame(r), cam.to_frame(u), cam.to_frame(b))
}
fn view_cube(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
let t = Tokens::get();
let c = pos2(rect.right() - 80.0, rect.top() + 80.0);
let (r, u, b) = app.cam.basis();
let (r, u, b) = cube_basis(&app.cam);
let (wr, wu, _) = app.cam.basis();
let painter = ui.painter_at(rect);
let hover = ui.input(|i| i.pointer.hover_pos());
let targets = cube_targets();
let shapes = cube_projected(&targets, (r, u, b), c);
let hovered = hover.and_then(|h| cube_hit(&shapes, h));
// Axis triad from the cube's back corner, drawn first: the cube hides what is behind it.
let to2 = |p: Vec3| pos2(c.x + (p.dot(r) * CUBE_SCALE) as f32, c.y - (p.dot(u) * CUBE_SCALE) as f32);
// The corner is in the cube's frame, the axes are the world's.
let o = Vec3::new(-1.0, -1.0, -1.0);
let at = pos2(c.x + (o.dot(r) * CUBE_SCALE) as f32, c.y - (o.dot(u) * CUBE_SCALE) as f32);
for (d, col) in [(Vec3::X, colors::AXIS_X), (Vec3::Y, colors::AXIS_Y), (Vec3::Z, colors::AXIS_Z)] {
painter.line_segment([to2(o), to2(o + d * 2.6)], Stroke::new(2.0, Color32::from_rgb(col.0, col.1, col.2)));
let tip = at + vec2((d.dot(wr) * CUBE_SCALE * 2.6) as f32, -(d.dot(wu) * CUBE_SCALE * 2.6) as f32);
painter.line_segment([at, tip], Stroke::new(2.0, Color32::from_rgb(col.0, col.1, col.2)));
}
// Back to front: the faces and facets turned toward the camera.
for (i, pts) in shapes.iter().rev() {
@@ -1934,7 +1943,11 @@ fn view_cube(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
app.animate_view("home");
}
if let Some(dir) = clicked {
let to = app.cam.looking_from(dir);
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).
if dir.x == 0.0 && dir.y == 0.0 {
to.yaw = 0.0;
}
app.animate_to(to);
}
}
@@ -0,0 +1,16 @@
[
{"note": "With Y up (#3) the view cube turns with the world: its TOP is world +Y and clicking it looks down −Y in the standard plan orientation; FRONT looks along −Z."},
{"call": "engine.execute", "params": {"command": "solid.box", "params": {"length": 40, "width": 30, "height": 20}}},
{"call": "ui.set", "params": {"upAxis": "y"}},
{"call": "ui.view", "params": {"view": "home"}},
{"move": {"cube": [0, 0, 1]}},
{"expect": {"shown_handles": ["cube_hover:top"]}},
{"click": {"handle": "cube:top"}},
{"expect": {"camera": {"pitch_deg": 90, "yaw_deg": 0, "back": [0, 1, 0]}}},
{"call": "ui.view", "params": {"view": "home"}},
{"click": {"handle": "cube:front"}},
{"expect": {"camera": {"pitch_deg": 0, "yaw_deg": 0, "back": [0, 0, 1]}}},
{"call": "ui.view", "params": {"view": "home"}},
{"click": {"handle": "cube:top-front-right"}},
{"expect": {"camera": {"pitch_deg": 35.26, "yaw_deg": -45, "back": [0.57735, 0.57735, 0.57735]}}}
]