mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-03 00:42:46 -06:00
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:
@@ -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()),
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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" };
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user