forked from 1b-agent/opencadstudio-upstream
fix: feet-inch dynamic input, STRETCH preselection, ribbon title wrap
- Dynamic-input fields take ' and " and read 2'9" / 3' as lengths (#937) - STRETCH on a selection made by a crossing window stretches through that window and goes straight to the base point (#1068) - STRETCH's crossing-window steps show no coordinate boxes (#657) - Collapsed ribbon panel titles wrap at words, never mid-word (#920) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
178c7dff41
commit
1cbcc8f4c5
9 files changed
+67
-5
No files matched your search
+23
-1
@@ -976,7 +976,29 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
self.tabs[i].scene.wire_models_for(&handles)
|
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.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,6 +268,9 @@ pub(super) struct DocumentTab {
|
|||||||
/// `suspended_cmd` was parked by a transparent command (`'ZOOM`) and is
|
/// `suspended_cmd` was parked by a transparent command (`'ZOOM`) and is
|
||||||
/// restored as soon as the transparent one ends.
|
/// restored as soon as the transparent one ends.
|
||||||
pub(super) transparent_resume: bool,
|
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 {
|
impl DocumentTab {
|
||||||
@@ -669,6 +672,7 @@ impl DocumentTab {
|
|||||||
plugin_state: HashMap::new(),
|
plugin_state: HashMap::new(),
|
||||||
suspended_cmd: None,
|
suspended_cmd: None,
|
||||||
transparent_resume: false,
|
transparent_resume: false,
|
||||||
|
last_crossing_window: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -381,6 +381,10 @@ impl Parser {
|
|||||||
/// Evaluate a string as an f64 expression.
|
/// Evaluate a string as an f64 expression.
|
||||||
/// Returns None on parse error.
|
/// Returns None on parse error.
|
||||||
pub fn eval_number(input: &str) -> Option<f64> {
|
pub fn eval_number(input: &str) -> Option<f64> {
|
||||||
|
// 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 tokens = tokenize(input.trim()).ok()?;
|
||||||
let mut parser = Parser::new(tokens);
|
let mut parser = Parser::new(tokens);
|
||||||
let result = parser.parse_expr(0).ok()?;
|
let result = parser.parse_expr(0).ok()?;
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
let dyn_field_char = !s.is_empty()
|
let dyn_field_char = !s.is_empty()
|
||||||
&& s.chars().all(|c| {
|
&& s.chars().all(|c| {
|
||||||
c.is_ascii_digit()
|
c.is_ascii_digit()
|
||||||
|| matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')')
|
|| matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')' | '\'' | '"')
|
||||||
});
|
});
|
||||||
if dyn_field_char
|
if dyn_field_char
|
||||||
&& self.command_line.input.is_empty()
|
&& self.command_line.input.is_empty()
|
||||||
|
|||||||
@@ -191,7 +191,9 @@ impl OpenCADStudio {
|
|||||||
let picks_object = self.tabs[i]
|
let picks_object = self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.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);
|
.unwrap_or(false);
|
||||||
let has_base = self.last_point.is_some();
|
let has_base = self.last_point.is_some();
|
||||||
// While aligned to an OTRACK ray, the point step reads a single
|
// While aligned to an OTRACK ray, the point step reads a single
|
||||||
|
|||||||
@@ -4728,6 +4728,9 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some((fence, window)) = drag_geometry {
|
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| {
|
let result = self.tabs[i].active_cmd.as_mut().and_then(|command| {
|
||||||
command.set_shift(self.shift_down);
|
command.set_shift(self.shift_down);
|
||||||
command.on_drag_selection(&fence, window)
|
command.on_drag_selection(&fence, window)
|
||||||
|
|||||||
@@ -2908,6 +2908,12 @@ pub trait CadCommand: Send {
|
|||||||
false
|
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)
|
/// The already-picked first corner of the selection window (world space)
|
||||||
/// while `window_corner_pick()` is true and the opposite corner is being
|
/// 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
|
/// dragged. The host projects it and draws a filled selection marquee to the
|
||||||
|
|||||||
@@ -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<Handle>,
|
||||||
|
wire_models: Vec<WireModel>,
|
||||||
|
window: (DVec3, DVec3),
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
handles,
|
||||||
|
wire_models,
|
||||||
|
windows: vec![window],
|
||||||
|
step: Step::Base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Continue gathering crossing windows after the host has resolved the
|
/// Continue gathering crossing windows after the host has resolved the
|
||||||
/// entities touched by the latest window.
|
/// entities touched by the latest window.
|
||||||
pub fn with_windows(
|
pub fn with_windows(
|
||||||
@@ -223,6 +238,12 @@ impl CadCommand for StretchCommand {
|
|||||||
matches!(self.step, Step::WindowCorner1 | Step::WindowCorner2(_))
|
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<DVec3> {
|
fn window_first_corner(&self) -> Option<DVec3> {
|
||||||
// Expose the first corner so the host draws a filled crossing marquee to
|
// 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.
|
// the cursor, matching a normal box selection instead of a bare outline.
|
||||||
|
|||||||
@@ -1569,7 +1569,7 @@ fn collapse_button<'a>(
|
|||||||
.size(9)
|
.size(9)
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.align_x(iced::Center)
|
.align_x(iced::Center)
|
||||||
.wrapping(iced::advanced::text::Wrapping::WordOrGlyph)
|
.wrapping(iced::advanced::text::Wrapping::Word)
|
||||||
.style(muted_text_style),
|
.style(muted_text_style),
|
||||||
crate::ui::icons::themed_secondary_arrow_down(8.0),
|
crate::ui::icons::themed_secondary_arrow_down(8.0),
|
||||||
]
|
]
|
||||||
@@ -1640,7 +1640,7 @@ fn collapse_button<'a>(
|
|||||||
.size(9)
|
.size(9)
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.align_x(iced::Center)
|
.align_x(iced::Center)
|
||||||
.wrapping(iced::advanced::text::Wrapping::WordOrGlyph)
|
.wrapping(iced::advanced::text::Wrapping::Word)
|
||||||
.style(muted_text_style),
|
.style(muted_text_style),
|
||||||
crate::ui::icons::themed_secondary_arrow_down(8.0),
|
crate::ui::icons::themed_secondary_arrow_down(8.0),
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in new issue
Block a user