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:
1 parent
d1cdc4d061
commit
9dd901649c
3 files changed
+44
-6
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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]}}}
|
||||
]
|
||||
Reference in new issue
Block a user