From a76775bf2bf124640feab3bf8f8cfdf1b94f0cf7 Mon Sep 17 00:00:00 2001 From: MINJAE KIM Date: Mon, 19 Oct 2026 15:54:51 +0900 Subject: [PATCH] Holding Shift overrides ortho while it is held, as in AutoCAD (#1198) At a point prompt with a base point, holding Shift turns ortho on when ORTHOMODE is off and off when it is on; releasing it restores the setting. ORTHOMODE itself is never changed. While the override turns ortho on, polar tracking is off, as with ortho on. The UI already passed the Shift state to Session::snap_cursor (for POLARMODE bit 8's Shift to acquire), so the change is in the engine only: snap_cursor applies the override to its copy of the settings. Window corners (ZOOM Window) and prompts without a base point are unaffected. Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/engine/src/snap/tracking.rs | 8 +++++-- crates/engine/tests/snap_tracking.rs | 33 ++++++++++++++++++++++++++++ docs/ui-parity.md | 3 ++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/snap/tracking.rs b/crates/engine/src/snap/tracking.rs index e0740d7b..9db1873f 100644 --- a/crates/engine/src/snap/tracking.rs +++ b/crates/engine/src/snap/tracking.rs @@ -770,7 +770,8 @@ pub struct CursorQuery { pub tol: f64, /// Seconds on any steady clock (acquisition timing). pub now: f64, - /// Shift is held. + /// Shift is held: it overrides ortho for as long as it is held, and acquires tracking + /// points with POLARMODE bit 8. pub shift: bool, } @@ -780,7 +781,10 @@ impl crate::Session { /// tracking, Extension, Parallel, Apparent Intersection), then ortho. pub fn snap_cursor(&mut self, raw: Vec2, q: &CursorQuery) -> CursorSnap { // Snap and grid spacing are the drawing's (SNAPUNIT/GRIDUNIT live in its header). - let s = self.drafting_settings(); + let mut s = self.drafting_settings(); + // Holding Shift overrides ortho while it is held: on when ORTHOMODE is off, off when it + // is on (ORTHOMODE itself is unchanged). Ortho on also turns polar tracking off. + s.orthomode ^= q.shift; let angbase = self.angle_settings().angbase; let Ok(st) = self.state() else { return CursorSnap { point: raw, ..Default::default() } }; let (doc, space) = (st.doc.clone(), st.edit_space()); diff --git a/crates/engine/tests/snap_tracking.rs b/crates/engine/tests/snap_tracking.rs index 2f2bbd90..3843002d 100644 --- a/crates/engine/tests/snap_tracking.rs +++ b/crates/engine/tests/snap_tracking.rs @@ -302,6 +302,39 @@ fn shift_to_acquire() { assert_eq!(s.tracking.acquired.len(), 1); } +#[test] +fn shift_overrides_ortho_while_held() { + let mut s = session(0); + let base = Some(v(0.0, 0.0)); + let held = |base| CursorQuery { shift: true, ..query(base, 100.0) }; + // Ortho off: Shift turns it on. + assert_eq!(at(&mut s, v(5.0, 1.0), base).point, v(5.0, 1.0)); + assert_eq!(s.snap_cursor(v(5.0, 1.0), &held(base)).point, v(5.0, 0.0)); + // Ortho on: Shift turns it off. + s.execute("ortho", &json!({ "on": true })).unwrap(); + assert_eq!(at(&mut s, v(5.0, 1.0), base).point, v(5.0, 0.0)); + assert_eq!(s.snap_cursor(v(5.0, 1.0), &held(base)).point, v(5.0, 1.0)); + assert!(s.settings.orthomode, "the override leaves ORTHOMODE as it was"); + // No base point, or a window's first corner: nothing to constrain. + assert_eq!(s.snap_cursor(v(5.0, 1.0), &held(None)).point, v(5.0, 1.0)); + s.execute("ortho", &json!({ "on": false })).unwrap(); + let corner = CursorQuery { corner: true, ..held(base) }; + assert_eq!(s.snap_cursor(v(5.0, 1.0), &corner).point, v(5.0, 1.0)); +} + +#[test] +fn shift_ortho_override_wins_over_polar() { + let mut s = session(0); + s.execute("polar", &json!({ "on": true })).unwrap(); + setvar(&mut s, "POLARANG", json!(45)); + let base = Some(v(0.0, 0.0)); + // Near the 45° polar path the cursor follows it; with Shift held ortho does instead. + let free = at(&mut s, v(5.0, 5.05), base); + assert!(free.track.is_some() && free.point.near(v(5.025, 5.025), 1e-9), "{free:?}"); + let held = s.snap_cursor(v(5.0, 5.05), &CursorQuery { shift: true, ..query(base, 100.0) }); + assert_eq!((held.point, held.track), (v(0.0, 5.05), None)); +} + #[test] fn sysvars_polarmode_polaraddang_autosnap() { let mut s = Session::new(); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 3e3bac74..6776f70f 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -83,7 +83,7 @@ ignored; there are no context menus. Remaining: **≈ 110–170 Opus 5.5 hours** | Feature | AutoCAD | CADCraft | |---|---|---| -| Ortho (F8) | yes | yes (`snap.rs:503`) | +| Ortho (F8) | yes, Shift overrides it while held | yes; holding Shift turns it on or off while held (`Session::snap_cursor`, `snap/tracking.rs`) | | Polar tracking increment | yes | yes, measured from ANGBASE; dotted path and "Polar: d < a°" tooltip (`snap/tracking.rs`, `canvas.rs` `draw_tracking`) | | Additional polar angles, relative to last segment | yes | additional angles yes (POLARADDANG with POLARMODE 4); **relative to the last segment no** | | Polar distance snap (PolarSnap) | yes | **no** | @@ -206,6 +206,7 @@ down-arrow options menu (DYNPROMPT), and DYNDIM. | 2026-10-13 | minor | Filled-region highlight boundaries are traversed in deterministic coordinate order, fixing dash-phase jumps on CPU hover/selection redraws and GPU highlight rebuilds; CPU dash-position, pixel and GPU mesh regressions | | 2026-10-14 | minor | Selection cycling (#1143): the list and badge of SELECTIONCYCLING, Shift+Space through overlapping objects (#1145) | | 2026-10-14 | minor | Hatch Edit dialog adds boundaries from picked points or closed objects and removes selected loops | +| 2026-10-14 | minor | Holding Shift overrides ortho while it is held (on when ORTHOMODE is off, off when it is on), as in AutoCAD | | 2026-10-13 | minor | Options ▸ Selection adds Select using two clicks only; PICKDRAG 0 disables drag-window creation/completion, 2 preserves clicks or drag, and the preference survives restart; UI pointer and profile regressions | | 2026-10-12 | minor | Selected and hovered objects redrawn in the selection colour (text and arrowheads included); Options ▸ Selection laid out as AutoCAD's: Object grouping, Show grips, grip object limit, selection preview, Visual Effect Settings (selection area, preview filter) | | 2026-10-12 | minor | Animation effects (dialog and menu fades, smoothed wheel scrolling and ⌘/Ctrl+wheel zoom) follow the operating system's setting; View ▸ Accessibility ▸ Animation Effects chooses System, On or Off |