Merge pull request #1785 from ramox81/codex/selection-found-messages

Selection: report what each pick found while a command gathers objects

Co-Authored-By: Claude Opus 5.5 <[email protected]>

# Conflicts:
#	src/locale_catalog.rs
This commit is contained in:
Hakan Seven committed 2026-10-14 23:57:20 +03:00
commit b5fb42e9f8
28 files changed
+171 -16

No files matched your search

+1 -3
View File
@@ -1244,6 +1244,7 @@ impl OpenCADStudio {
bounds,
|point| self.cursor_model_point(i, &edit_cam, point, bounds),
);
self.last_pick = Some((handles.clone(), self.select_remove_mode));
if self.select_remove_mode {
for h in &handles {
self.tabs[i].scene.deselect_entity(*h);
@@ -1261,9 +1262,6 @@ impl OpenCADStudio {
.into_iter()
.map(|(h, _)| h)
.collect();
let count = handles.len();
self.command_line
.push_info(crate::tf!("{count} object(s) found.").as_ref());
self.feed_command(StepInput::SelectionComplete(selected))
}
+55 -3
View File
@@ -574,6 +574,56 @@ impl OpenCADStudio {
StepInput::SelectionComplete(kept)
}
/// What one selection step picked, as the reference words it: `N found`
/// for the first step, `N found, M total`, `N found (D duplicate), M
/// total` when some were chosen already, `N found, R removed, M total` in
/// remove mode. Returns the command's key while it gathers.
fn report_selection_found(&mut self, input: &StepInput) -> Option<usize> {
let StepInput::SelectionComplete(handles) = input else {
return None;
};
// A pick belongs to this step only; never to a later one.
let last_pick = self.last_pick.take();
let command = self.tabs[self.active_tab].active_cmd.as_ref()?;
if !command.is_selection_gathering() {
return None;
}
let key = &**command as *const dyn crate::command::CadCommand as *const () as usize;
let previous: rustc_hash::FxHashSet<Handle> = match &self.selection_report {
Some((k, previous)) if *k == key => previous.iter().copied().collect(),
_ => Default::default(),
};
let now: rustc_hash::FxHashSet<Handle> = handles.iter().copied().collect();
let total = now.len();
// What the step picked: its own objects when the pick says so, else
// what the selection gained or lost.
let (picked, remove) = match last_pick {
Some((picked, remove)) => (picked.into_iter().collect(), remove),
None => {
let lost: rustc_hash::FxHashSet<Handle> =
previous.difference(&now).copied().collect();
if lost.is_empty() {
(now.difference(&previous).copied().collect(), false)
} else {
(lost, true)
}
}
};
let found = picked.len();
let known = picked.intersection(&previous).count();
let line = if found == 0 || previous.is_empty() && !remove {
crate::tf!("{found} found")
} else if remove && known > 0 {
crate::tf!("{found} found, {known} removed, {total} total")
} else if !remove && known > 0 {
crate::tf!("{found} found ({known} duplicate), {total} total")
} else {
crate::tf!("{found} found, {total} total")
};
self.command_line.push_info(line.as_ref());
Some(key)
}
/// `feed_command`, also reporting whether the step actually took the input.
/// Only a `Text` token can come back unclaimed (`on_text_input` returning
/// `None`), which is what lets the caller read it as something else — a
@@ -582,6 +632,7 @@ impl OpenCADStudio {
&mut self,
input: StepInput,
) -> (Task<Message>, bool) {
let gathering = self.report_selection_found(&input);
// A command that takes associative dimensions only keeps just those:
// the rest leave the selection and the count of what went is reported,
// as the reference does while gathering.
@@ -589,6 +640,9 @@ impl OpenCADStudio {
let input = self.filter_block_reference_selection(input);
let input = self.filter_locked_point_cloud_selection(input);
let input = self.filter_command_entity_selection(input);
if let (Some(key), StepInput::SelectionComplete(kept)) = (gathering, &input) {
self.selection_report = Some((key, kept.clone()));
}
// XCLIP / CLIP learn which chosen references carry a clip already.
{
let i = self.active_tab;
@@ -1045,12 +1099,10 @@ impl OpenCADStudio {
);
return Some(Task::none());
}
let count = add.len();
self.last_pick = Some((add.clone(), false));
for h in add {
self.tabs[i].scene.select_entity(h, false);
}
self.command_line
.push_info(crate::tf!("{count} object(s) added to selection.").as_ref());
self.refresh_properties();
let handles: Vec<Handle> = self.tabs[i]
.scene
+8
View File
@@ -800,6 +800,12 @@ pub(super) struct OpenCADStudio {
block_freq: std::collections::HashMap<String, u32>,
/// BLOCKMRULIST: how many recent blocks the palette keeps (0–100).
pub(crate) block_mru_list: u8,
/// The selection the gathering command had after the last pick (keyed by
/// the command), for the `N found, M total` line.
pub(crate) selection_report: Option<(usize, Vec<codec::Handle>)>,
/// The objects the last selection step picked (a click, a window, a
/// fence, Last / Previous / All) and whether it removed them.
pub(crate) last_pick: Option<(Vec<codec::Handle>, bool)>,
/// The Edit Reference dialog's remembered settings.
pub(crate) refedit_options: crate::app::settings::RefEditOptions,
/// BLOCKREDEFINEMODE (0–2).
@@ -4696,6 +4702,8 @@ impl OpenCADStudio {
block_mru: Vec::new(),
block_freq: std::collections::HashMap::new(),
block_mru_list: 50,
selection_report: None,
last_pick: None,
refedit_options: Default::default(),
block_redefine_mode: 1,
block_navigate: ".".to_string(),
+14
View File
@@ -5230,6 +5230,10 @@ properties={:.1}ms picked={}",
// lasso adds, Shift+lasso removes. An empty lasso
// leaves the current selection untouched so a stray
// drag never discards hard-won picks.
if is_gathering {
self.last_pick =
Some((handles.clone(), self.shift_down || self.select_remove_mode));
}
if self.shift_down || self.select_remove_mode {
self.tabs[i].scene.deselect_entities(&handles);
} else {
@@ -5372,6 +5376,12 @@ properties={:.1}ms picked={}",
let hit =
hit.filter(|&h| self.tabs[i].scene.passes_selection_filter(h));
if let Some(handle) = hit {
if is_gathering {
self.last_pick = Some((
vec![handle],
self.shift_down || self.select_remove_mode,
));
}
// Individual picks accumulate (issue #47):
// each plain click adds to the selection,
// Shift+click removes the picked entity.
@@ -5504,6 +5514,10 @@ was_selected={}",
// entities. An empty box leaves the selection alone
// so an accidental empty drag never discards it.
// PICKADD 0 (#226): a plain box REPLACES instead.
if is_gathering {
self.last_pick =
Some((handles.clone(), self.shift_down || self.select_remove_mode));
}
if self.shift_down || self.select_remove_mode {
self.tabs[i].scene.deselect_entities(&handles);
} else {
+2
View File
@@ -6,6 +6,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"{} entities added" => Some(("ref-edit", "entities-added")),
"Use REFCLOSE or the Refedit toolbar to end reference editing session." => Some(("ref-edit", "use-refclose")),
"Specify opposite corner:" => Some(("ref-edit", "opposite-corner")),
"{found} found, {known} removed, {total} total" => Some(("selection-report", "found-removed-total")),
"{found} found ({known} duplicate), {total} total" => Some(("selection-report", "found-duplicate-total")),
"Add to Working Set" => Some(("ref-edit", "add-to-working-set")),
"Remove from Working Set" => Some(("ref-edit", "remove-from-working-set")),
"Select reference:" => Some(("ref-edit", "select-reference")),
+3 -10
View File
@@ -158,17 +158,10 @@ impl CadCommand for CountCommand {
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
// The selection arrives whole after every pick: `1 found`, then
// `1 found, 2 total`.
let found = handles.iter().filter(|h| !self.targets.contains(h)).count();
let first = self.targets.is_empty();
// The selection arrives whole after every pick; the driver reports
// what each pick found.
self.targets = handles;
let total = self.targets.len();
CmdResult::ReportMeasurement(if first {
crate::tf!("{found} found").into_owned()
} else {
crate::tf!("{found} found, {total} total").into_owned()
})
CmdResult::NeedPoint
}
fn needs_entity_pick(&self) -> bool {