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 {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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 mut parser = Parser::new(tokens);
|
||||
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()
|
||||
&& s.chars().all(|c| {
|
||||
c.is_ascii_digit()
|
||||
|| matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')')
|
||||
|| matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')' | '\'' | '"')
|
||||
});
|
||||
if dyn_field_char
|
||||
&& self.command_line.input.is_empty()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
/// 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<DVec3> {
|
||||
// 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.
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
Reference in new issue
Block a user