Keep rectangle opposite corners free of ortho (STRETCH window and friends)

With ORTHOMODE on, the opposite corner of STRETCH's crossing window was bent
onto an axis, so the window collapsed and caught nothing. The prompt set only
the rubber-band base; it now marks it as a window corner (Prompt::corner), as
ZOOM Window and RECTANG already do.

Same fix for the other two-corner rectangles: the Window/Crossing/Box selection
modes, PLOTWINDOW, MVIEW (and its Restore), -VIEW Window and REVCLOUD
Rectangular. Regression tests in crates/engine/tests/stretch_window_ortho.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sorin
2026-10-18 13:17:25 +03:00
co-authored by Claude Opus 5.5
parent b23d362f6a
commit cda7381c3d
7 changed files with 88 additions and 7 deletions
+3 -1
View File
@@ -1057,7 +1057,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()),
+2 -2
View File
@@ -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),
}
}
+1 -1
View File
@@ -486,7 +486,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"),
+1 -1
View File
@@ -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<Step> {
+1 -1
View File
@@ -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" };
+1 -1
View File
@@ -419,7 +419,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),
}
}
@@ -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:?}");
}
}