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 |