Alias navigation preset uses Shift+Alt with LMB/MMB/RMB (Fixes #304) (#306)

Navigation presets are now a table of bindings (button, required modifier
keys, camera mode) instead of string matches. The Alias preset binds
Shift+Alt+left to orbit, Shift+Alt+middle to pan and Shift+Alt+right to
zoom. A left press with a binding's keys is latched until release, so that
drag neither box-selects nor drags sketch geometry; plain left drags behave
as before. Shift only requests snapping when the binding without Shift
still orbits.

The scenario harness sends Alt with ui.drag/ui.click, and camera checks can
assert panned/zoomed since the last camera step. The user guide notes Alt+drag
grabs by KDE and GNOME and how to free them.


Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-12 10:33:53 -04:00
1 parent 2cac9cad6a
commit 611422e4df
7 files changed
+278 -83

No files matched your search

+31 -2
View File
@@ -134,6 +134,8 @@ pub struct Harness {
queue: Option<std::sync::mpsc::Sender<ControlRequest>>,
time: f64,
size: egui::Vec2,
/// The camera as the last `camera` step left it.
cam_mark: Option<solvecraft_engine::render::Camera>,
}
impl Default for Harness {
@@ -162,6 +164,7 @@ impl Harness {
queue: None,
time: 0.0,
size: egui::vec2(1600.0, 1000.0),
cam_mark: None,
};
WIDTHS.with(|m| m.borrow_mut().clear());
h.install();
@@ -361,7 +364,7 @@ impl Harness {
let [x, y] = self.at(at)?;
// Hover first, as a hand does.
self.call("ui.move", json!({"x": x, "y": y}));
let mut p = json!({"x": x, "y": y, "double": flag("double"), "shift": flag("shift"), "ctrl": flag("ctrl")});
let mut p = json!({"x": x, "y": y, "double": flag("double"), "shift": flag("shift"), "ctrl": flag("ctrl"), "alt": flag("alt")});
if let Some(b) = s.get("button") {
p["button"] = b.clone();
}
@@ -382,7 +385,10 @@ impl Harness {
let [x0, y0] = self.at(d.get(0).unwrap_or(&Value::Null))?;
let [x1, y1] = self.at(d.get(1).unwrap_or(&Value::Null))?;
let steps = s.get("steps").cloned().unwrap_or(json!(8));
self.call("ui.drag", json!({"x0": x0, "y0": y0, "x1": x1, "y1": y1, "steps": steps, "shift": flag("shift"), "ctrl": flag("ctrl")}))
self.call(
"ui.drag",
json!({"x0": x0, "y0": y0, "x1": x1, "y1": y1, "steps": steps, "shift": flag("shift"), "ctrl": flag("ctrl"), "alt": flag("alt")}),
)
} else if let Some(m) = s.get("queued").and_then(Value::as_str) {
let (v, frames) = self.call_queued(m, s.get("params").cloned().unwrap_or(json!({})));
let min = s.get("min_frames").and_then(Value::as_u64).unwrap_or(0);
@@ -432,6 +438,8 @@ impl Harness {
}
self.app.cam_anim = None;
self.frames(2);
// Where `camera.panned` and `camera.zoomed` checks measure from.
self.cam_mark = Some(self.app.cam);
return Ok(());
} else if let Some(v) = s.get("view_anim").and_then(Value::as_str) {
return self.view_anim(v, s.get("max_deg").and_then(Value::as_f64).unwrap_or(10.0));
@@ -611,6 +619,27 @@ 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}"));
}
// Since the last `camera` step: "panned" (bool: the target moved) and "zoomed"
// ("in", "out" or "no": the eye's distance from the target).
if !v["panned"].is_null() || !v["zoomed"].is_null() {
let mark = h.cam_mark.ok_or("camera.panned/zoomed need a `camera` step first")?;
let c = &h.app.cam;
let moved = c.target.dist(mark.target) > 1e-6 * mark.distance.max(1.0);
if v["panned"].as_bool().is_some_and(|want| want != moved) {
return Err(format!("camera panned: got {moved}, want {}", v["panned"]));
}
let r = c.distance / mark.distance;
let zoomed = if r < 1.0 - 1e-6 {
"in"
} else if r > 1.0 + 1e-6 {
"out"
} else {
"no"
};
if v["zoomed"].as_str().is_some_and(|want| want != zoomed) {
return Err(format!("camera zoomed: got {zoomed} (distance ×{r:.4}), want {}", v["zoomed"]));
}
}
// "back": the world direction from the target to the eye.
if let Some(want) = v["back"].as_array() {
let b = h.app.cam.back();