diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 7bed43c6..3ae4a7f7 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -357,7 +357,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) -> .and_then(|id| st.sketch(id)) .map(|ss| ss.plane.to_world(solidcraft_engine::geom::Vec2::new(num(s, 0), num(s, 1)))) } else if let Some(n) = p.get("plane").and_then(Value::as_str) { - crate::viewport::origin_planes(app).into_iter().find(|(name, _, _)| *name == n).map(|(_, _, q)| (q[0] + q[1] + q[2] + q[3]) / 4.0) + crate::viewport::origin_plane_target(app, &proj, n) } else if let Some(n) = p.get("axis").and_then(Value::as_str) { // Along an origin axis, on its solid part. let size = crate::selection::origin_size(app.cam.half_height()); diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index a864060e..0c672dff 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -19,6 +19,10 @@ use crate::{SolveApp, icons}; #[path = "viewport_appearance_tests.rs"] mod appearance_tests; +#[cfg(test)] +#[path = "viewport_plane_target_tests.rs"] +mod plane_target_tests; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum NavMode { Orbit, @@ -548,6 +552,16 @@ pub fn origin_planes(app: &SolveApp) -> Vec<(&'static str, Vec3, [Vec3; 4])> { .collect() } +/// A point on origin plane `name`'s square that a click at its screen point picks: the middle, +/// or else the middle of a quarter the other planes don't cover from this view (in an iso view +/// the XZ square hides the middle of the XY one). The middle when no candidate is picked. +pub fn origin_plane_target(app: &SolveApp, proj: &Proj, name: &str) -> Option { + let (_, _, q) = origin_planes(app).into_iter().find(|(n, _, _)| *n == name)?; + let mid = (q[0] + q[1] + q[2] + q[3]) / 4.0; + let picked = |w: &Vec3| proj.to_screen(*w).is_some_and(|p| matches!(pick(app, proj, p).first(), Some(Hit::Plane { name: n, .. }) if n == name)); + Some(std::iter::once(mid).chain(q.iter().map(|c| (mid + *c) / 2.0)).find(picked).unwrap_or(mid)) +} + pub fn origin_axes(app: &SolveApp) -> Vec<(&'static str, Vec3)> { if !app.origin_visible() { return Vec::new(); diff --git a/crates/ui-egui/src/viewport_plane_target_tests.rs b/crates/ui-egui/src/viewport_plane_target_tests.rs new file mode 100644 index 00000000..72344c3b --- /dev/null +++ b/crates/ui-egui/src/viewport_plane_target_tests.rs @@ -0,0 +1,22 @@ +use crate::scenario::Harness; +use serde_json::json; + +/// `ui.at {"plane": …}` gives a point that a click picks that plane at, even where another +/// origin plane hides the middle of its square (seen from the front right and a little lower +/// than iso, the XZ square covers the middle of the XY one). +#[test] +fn clicking_an_origin_plane_target_sketches_on_that_plane() { + for (yaw, pitch) in [(-0.785, 0.615), (-1.1, 0.5), (-0.45, 0.5), (2.4, 0.6), (-2.4, 0.4)] { + for (plane, normal) in [("XY", [0.0, 0.0, 1.0]), ("XZ", [0.0, 1.0, 0.0]), ("YZ", [1.0, 0.0, 0.0])] { + let mut h = Harness::new(); + h.app.cam.yaw = yaw; + h.app.cam.pitch = pitch; + h.step(&json!({"start":"sketch.create"})).unwrap(); + h.step(&json!({"click":{"plane":plane}})).unwrap(); + let state = h.app.session.world_state(); + let n = state.sketches.first().map(|s| s.plane.normal()).unwrap_or_default(); + let along = (n.x * normal[0] + n.y * normal[1] + n.z * normal[2]).abs(); + assert!(along > 0.999, "yaw {yaw} pitch {pitch} {plane}: sketched on a plane with normal {n:?}"); + } + } +}