diff --git a/crates/engine/src/cmd/draw2.rs b/crates/engine/src/cmd/draw2.rs index 68dfd5db..388b8ba5 100644 --- a/crates/engine/src/cmd/draw2.rs +++ b/crates/engine/src/cmd/draw2.rs @@ -1083,7 +1083,9 @@ impl Interactive for RevcloudM { let first_kw: &[&str] = &["Arc length", "Object", "Rectangular", "Polygonal", "Freehand", "Style"]; match (self.kind, self.pts.len()) { (CloudKind::Rect, 0) => Prompt::new("Specify first corner point", Accept::POINT).kw(first_kw).default("Object"), - (CloudKind::Rect, _) => Prompt::new("Specify opposite corner", Accept::POINT).base_opt(self.pts.first().copied()), + (CloudKind::Rect, _) => { + Prompt { corner: true, ..Prompt::new("Specify opposite corner", Accept::POINT).base_opt(self.pts.first().copied()) } + } (CloudKind::Poly, 0) => Prompt::new("Specify start point", Accept::POINT).kw(first_kw).default("Object"), (CloudKind::Poly, 1) => Prompt::new("Specify next point", Accept::POINT).base_opt(self.pts.last().copied()), (CloudKind::Poly, _) => Prompt::new("Specify next point", Accept::POINT).kw(&["Undo"]).base_opt(self.pts.last().copied()), diff --git a/crates/engine/src/cmd/layout/viewports.rs b/crates/engine/src/cmd/layout/viewports.rs index 7f47c4e3..e82d881f 100644 --- a/crates/engine/src/cmd/layout/viewports.rs +++ b/crates/engine/src/cmd/layout/viewports.rs @@ -637,13 +637,13 @@ impl Interactive for MviewM { Stage::Arrange => Prompt::new("Enter viewport arrangement", curves::KW) .kw(&["Horizontal", "Vertical", "Above", "Below", "Left", "Right"]) .default("Right"), - Stage::Opposite(a) => Prompt::new("Specify opposite corner", Accept::POINT).base(*a), + Stage::Opposite(a) => Prompt::new("Specify opposite corner", Accept::POINT).corner(*a), Stage::Polygon(pi) => pi.prompt(), Stage::Object => Prompt::new("Select object to clip viewport", Accept::POINT), Stage::Lock => Prompt::new("Lock the view of viewports?", curves::KW).kw(&["ON", "OFF"]), Stage::RestoreName => Prompt::new("Enter viewport configuration name", Accept::TEXT).kw(&["?"]).default(VportConfig::ACTIVE), Stage::RestoreCorner(_) => Prompt::new("Specify first corner", Accept::POINT).kw(&["Fit"]).default("Fit"), - Stage::RestoreOpposite(_, a) => Prompt::new("Specify opposite corner", Accept::POINT).base(*a), + Stage::RestoreOpposite(_, a) => Prompt::new("Specify opposite corner", Accept::POINT).corner(*a), Stage::Select { .. } => Prompt::new("Select viewports", Accept::POINT), } } diff --git a/crates/engine/src/cmd/modify.rs b/crates/engine/src/cmd/modify.rs index 059beb6a..715ad895 100644 --- a/crates/engine/src/cmd/modify.rs +++ b/crates/engine/src/cmd/modify.rs @@ -517,7 +517,7 @@ impl Interactive for StretchM { return m.prompt(); } match (self.phase, self.corner) { - (StretchPhase::Select, Some(c)) => Prompt::new("Specify opposite corner", Accept::POINT).base(c), + (StretchPhase::Select, Some(c)) => Prompt::new("Specify opposite corner", Accept::POINT).corner(c), (StretchPhase::Select, None) => Prompt::new(if self.removing { "Remove objects" } else { "Select objects" }, Accept::POINT), (StretchPhase::Base, _) => Prompt::new("Specify base point", Accept::POINT).kw(&["Displacement"]).default("Displacement"), (StretchPhase::Displacement, _) => Prompt::new("Specify displacement", Accept::POINT).default("0,0"), diff --git a/crates/engine/src/cmd/plotters.rs b/crates/engine/src/cmd/plotters.rs index 454b977a..762f6506 100644 --- a/crates/engine/src/cmd/plotters.rs +++ b/crates/engine/src/cmd/plotters.rs @@ -365,7 +365,7 @@ impl Interactive for PlotWindowM { fn prompt(&self, _s: &Session) -> Prompt { match self.first { None => Prompt::new("Specify first corner", Accept::POINT), - Some(a) => Prompt::new("Specify opposite corner", Accept::POINT).base(a), + Some(a) => Prompt::new("Specify opposite corner", Accept::POINT).corner(a), } } fn input(&mut self, s: &mut Session, i: Input) -> Result { diff --git a/crates/engine/src/cmd/view/named_views.rs b/crates/engine/src/cmd/view/named_views.rs index 40c25632..ef2750ef 100644 --- a/crates/engine/src/cmd/view/named_views.rs +++ b/crates/engine/src/cmd/view/named_views.rs @@ -279,7 +279,7 @@ impl Interactive for ViewM { Stage::Restore => Prompt::new("Enter view name to restore", Accept::TEXT), Stage::Save | Stage::WindowName => Prompt::new("Enter view name to save", Accept::TEXT), Stage::WindowFirst => Prompt::new("Specify first corner", Accept::POINT), - Stage::WindowSecond(a) => Prompt::new("Specify opposite corner", Accept::POINT).base(a), + Stage::WindowSecond(a) => Prompt::new("Specify opposite corner", Accept::POINT).corner(a), Stage::Settings => Prompt::new("Enter an option", Accept::TEXT).kw(&["Layer snapshot"]), Stage::Snapshot => { let on = if s.settings.view_layer_snapshot { "Yes" } else { "No" }; diff --git a/crates/engine/src/select.rs b/crates/engine/src/select.rs index 7666ab54..d3db01c9 100644 --- a/crates/engine/src/select.rs +++ b/crates/engine/src/select.rs @@ -434,7 +434,7 @@ impl SelectMode { }; match last { None => Prompt::new(first, Accept::POINT), - Some(p) if self.is_box() => Prompt::new(next, Accept::POINT).base(p), + Some(p) if self.is_box() => Prompt::new(next, Accept::POINT).corner(p), Some(p) => Prompt::new(next, Accept::POINT).kw(&["Undo"]).base(p), } } diff --git a/crates/engine/tests/stretch_window_ortho.rs b/crates/engine/tests/stretch_window_ortho.rs new file mode 100644 index 00000000..70a7fdbf --- /dev/null +++ b/crates/engine/tests/stretch_window_ortho.rs @@ -0,0 +1,79 @@ +//! STRETCH's crossing window ignores ortho: the opposite corner is where the cursor is, so the +//! window keeps its area and catches what it crosses. The same holds for every rectangle picked by +//! two corners (PLOTWINDOW, MVIEW, -VIEW Window, REVCLOUD Rectangular). + +use cadcraft_engine::snap::tracking::CursorQuery; +use cadcraft_engine::{Input, Session}; +use cadcraft_geom::Vec2; +use serde_json::json; + +/// Move the cursor to `raw` at the current prompt, as the canvas does. +fn hover(s: &mut Session, raw: Vec2) -> Vec2 { + let p = s.current_prompt().unwrap(); + let q = CursorQuery { base: p.base, deferred: p.deferred, corner: p.corner, aperture: 1e-6, tol: 1e-6, now: 0.0, shift: false }; + let c = s.snap_cursor(raw, &q).point; + s.cursor = c; + c +} + +/// A session with ortho on, object snaps off and a line from 0,0 to 10,0. +fn ortho_session() -> Session { + let mut s = Session::new(); + s.execute("setvar", &json!({ "name": "OSMODE", "value": 0 })).unwrap(); + s.execute("setvar", &json!({ "name": "ORTHOMODE", "value": 1 })).unwrap(); + s.execute("line", &json!({ "points": [[0, 0], [10, 0]] })).unwrap(); + s +} + +/// Pick the window's opposite corner at `to`, stretch by 5,0 and return the extents. +fn stretch_from_corner(mut s: Session, to: Vec2) -> (Vec2, Vec2) { + assert_eq!(s.current_prompt().unwrap().message, "Specify opposite corner"); + let c = hover(&mut s, to); + assert!(c.near(to, 1e-9), "the corner is where the cursor is: {c:?}"); + s.input(Input::Point(c)).unwrap(); + s.input(Input::Enter).unwrap(); + for t in ["0,0", "5,0"] { + s.cmdline(t).unwrap(); + } + assert!(s.current_prompt().is_none(), "STRETCH is done, still at {:?}", s.current_prompt().map(|p| p.message)); + let b = s.doc().unwrap().extents(&s.space()); + (b.min, b.max) +} + +#[test] +fn stretch_implied_window_ignores_ortho() { + let mut s = ortho_session(); + s.cmdline("stretch").unwrap(); + // Right to left: a crossing window. + s.input(Input::Point(Vec2::new(12.0, 1.0))).unwrap(); + assert_eq!(stretch_from_corner(s, Vec2::new(8.0, -1.0)), (Vec2::ZERO, Vec2::new(15.0, 0.0)), "the line's end moved by 5"); +} + +#[test] +fn stretch_crossing_mode_ignores_ortho() { + let mut s = ortho_session(); + for t in ["stretch", "c", "8,-1"] { + s.cmdline(t).unwrap(); + } + assert_eq!(stretch_from_corner(s, Vec2::new(12.0, 1.0)), (Vec2::ZERO, Vec2::new(15.0, 0.0)), "the line's end moved by 5"); +} + +#[test] +fn rectangle_corner_prompts_ignore_ortho() { + for script in [&["plotwindow", "0,0"][..], &["mview", "0,0"], &["-view", "w", "v1", "0,0"], &["revcloud", "r", "0,0"]] { + let mut s = Session::new(); + s.execute("setvar", &json!({ "name": "OSMODE", "value": 0 })).unwrap(); + s.execute("setvar", &json!({ "name": "ORTHOMODE", "value": 1 })).unwrap(); + if script[0] == "mview" { + s.cmdline("layout s Layout1").unwrap(); + } + for t in script { + s.cmdline(t).unwrap(); + } + let p = s.current_prompt().unwrap(); + assert_eq!(p.message, "Specify opposite corner", "{script:?}"); + assert!(p.corner, "{script:?}: the opposite corner ignores ortho"); + let c = hover(&mut s, Vec2::new(10.0, 4.0)); + assert!(c.near(Vec2::new(10.0, 4.0), 1e-9), "{script:?}: the corner is where the cursor is: {c:?}"); + } +}