diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index bb0f570ff..8aa821cd5 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -976,7 +976,29 @@ impl OpenCADStudio { } else { self.tabs[i].scene.wire_models_for(&handles) }; - let new_cmd = StretchCommand::new(handles, wires); + // Selected by a crossing window just before: stretch through + // that window, no second one asked for (#1068). Every picked + // object must touch it, or the selection was changed since. + let window = self.tabs[i].last_crossing_window.and_then(|(lo, hi)| { + let touches = |wire: &crate::scene::WireModel| { + let [x0, y0, x1, y1] = wire.aabb.map(f64::from); + x0 <= hi[0] && x1 >= lo[0] && y0 <= hi[1] && y1 >= lo[1] + }; + (!handles.is_empty() + && handles.iter().all(|h| { + self.tabs[i].scene.wire_models_for(&[*h]).iter().any(touches) + })) + .then(|| { + ( + glam::DVec3::new(lo[0], lo[1], f64::NEG_INFINITY), + glam::DVec3::new(hi[0], hi[1], f64::INFINITY), + ) + }) + }); + let new_cmd = match window { + Some(window) => StretchCommand::preselected(handles, wires, window), + None => StretchCommand::new(handles, wires), + }; self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } diff --git a/src/app/document.rs b/src/app/document.rs index 605f13d8e..bc47e8ea0 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -268,6 +268,9 @@ pub(super) struct DocumentTab { /// `suspended_cmd` was parked by a transparent command (`'ZOOM`) and is /// restored as soon as the transparent one ends. pub(super) transparent_resume: bool, + /// Model-space corners of the last crossing window drawn with no command + /// running: STRETCH on that selection stretches through it (#1068). + pub(super) last_crossing_window: Option<([f64; 2], [f64; 2])>, } impl DocumentTab { @@ -669,6 +672,7 @@ impl DocumentTab { plugin_state: HashMap::new(), suspended_cmd: None, transparent_resume: false, + last_crossing_window: None, } } diff --git a/src/app/expr_eval.rs b/src/app/expr_eval.rs index be064efcd..d4ef17c6a 100644 --- a/src/app/expr_eval.rs +++ b/src/app/expr_eval.rs @@ -381,6 +381,10 @@ impl Parser { /// Evaluate a string as an f64 expression. /// Returns None on parse error. pub fn eval_number(input: &str) -> Option { + // Feet and inches (`2'9"`, `3'`, `9 1/2"`) are lengths, not arithmetic (#937). + if input.contains(['\'', '"']) { + return crate::entities::common::parse_typed_length(input); + } let tokens = tokenize(input.trim()).ok()?; let mut parser = Parser::new(tokens); let result = parser.parse_expr(0).ok()?; diff --git a/src/app/update/command.rs b/src/app/update/command.rs index ebbc09abb..feca3900f 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -269,7 +269,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { let dyn_field_char = !s.is_empty() && s.chars().all(|c| { c.is_ascii_digit() - || matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')') + || matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')' | '\'' | '"') }); if dyn_field_char && self.command_line.input.is_empty() diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index 5c014124c..8ac3e1241 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -191,7 +191,9 @@ impl OpenCADStudio { let picks_object = self.tabs[i] .active_cmd .as_ref() - .map(|c| c.needs_entity_pick() || c.needs_structure_point_pick()) + .map(|c| { + c.needs_entity_pick() || c.needs_structure_point_pick() || c.selects_by_window() + }) .unwrap_or(false); let has_base = self.last_point.is_some(); // While aligned to an OTRACK ray, the point step reads a single diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 83e56870d..3fa623065 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -4728,6 +4728,9 @@ impl OpenCADStudio { }; if let Some((fence, window)) = drag_geometry { + if self.tabs[i].active_cmd.is_none() { + self.tabs[i].last_crossing_window = window.filter(|_| box_crossing); + } let result = self.tabs[i].active_cmd.as_mut().and_then(|command| { command.set_shift(self.shift_down); command.on_drag_selection(&fence, window) diff --git a/src/command.rs b/src/command.rs index 63c3d0e6e..1ff3a281c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2908,6 +2908,12 @@ pub trait CadCommand: Send { false } + /// The step draws a window that selects objects rather than placing + /// geometry: no coordinate boxes at the cursor. Default `false`. + fn selects_by_window(&self) -> bool { + false + } + /// The already-picked first corner of the selection window (world space) /// while `window_corner_pick()` is true and the opposite corner is being /// dragged. The host projects it and draws a filled selection marquee to the diff --git a/src/modules/draw/modify/stretch.rs b/src/modules/draw/modify/stretch.rs index 394fb074b..82f6aac16 100644 --- a/src/modules/draw/modify/stretch.rs +++ b/src/modules/draw/modify/stretch.rs @@ -67,6 +67,21 @@ impl StretchCommand { } } + /// A selection made by a crossing window before the command: that window + /// marks what moves, so the command goes straight to the base point. + pub fn preselected( + handles: Vec, + wire_models: Vec, + window: (DVec3, DVec3), + ) -> Self { + Self { + handles, + wire_models, + windows: vec![window], + step: Step::Base, + } + } + /// Continue gathering crossing windows after the host has resolved the /// entities touched by the latest window. pub fn with_windows( @@ -223,6 +238,12 @@ impl CadCommand for StretchCommand { matches!(self.step, Step::WindowCorner1 | Step::WindowCorner2(_)) } + fn selects_by_window(&self) -> bool { + // Crossing windows select what stretches; dynamic input has nothing + // to enter there (#657). + self.window_corner_pick() + } + fn window_first_corner(&self) -> Option { // Expose the first corner so the host draws a filled crossing marquee to // the cursor, matching a normal box selection instead of a bare outline. diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index e7c69f818..245af7c9e 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -1569,7 +1569,7 @@ fn collapse_button<'a>( .size(9) .width(Fill) .align_x(iced::Center) - .wrapping(iced::advanced::text::Wrapping::WordOrGlyph) + .wrapping(iced::advanced::text::Wrapping::Word) .style(muted_text_style), crate::ui::icons::themed_secondary_arrow_down(8.0), ] @@ -1640,7 +1640,7 @@ fn collapse_button<'a>( .size(9) .width(Fill) .align_x(iced::Center) - .wrapping(iced::advanced::text::Wrapping::WordOrGlyph) + .wrapping(iced::advanced::text::Wrapping::Word) .style(muted_text_style), crate::ui::icons::themed_secondary_arrow_down(8.0), ]