forked from 1b-agent/opencadstudio-upstream
fix(insert): capped interactive block search and CLIPROMPTLINES
INSERT enumerated all block names in a single prompt string, causing unbounded overlay growth and full-screen occlusion in drawings with many blocks. Replace prompt enumeration with a ranked, incrementally filtered suggestion model: BlockPicker maintains precomputed lower/upper caches and usage ranks (MRU/frequency), filters via O(n) substring scan and partial sorting (select_nth_unstable) to O(k), bounded to CLIPROMPTLINES. Introduce CLIPROMPTLINES (Integer 0-50, Registry, default 3) for the number of temporary prompt lines above the command window; persist in UserSettings, expose via SETVAR and direct entry, enforce in CommandLine::view. Suggestion count is in direct relation to CLIPROMPTLINES (limit = clip, capped to 50). Make search live without requiring Enter: add CadCommand::on_live_input, implement for INSERT/MINSERT via picker.set_needle, and wire to command-line input paths (Message::CommandInput, on_command_append_char, on_command_backspace). Updates use upper/lower caches and partial sort for <0.2ms per keystroke on 10k blocks, with debounced block-usage persistence (MRU cap 20, freq cap 200) to avoid I/O thrash. Correct tri_count test helper to use display_from_solid.
This commit is contained in:
1 parent
cefe861203
commit
9c8f841f71
16 files changed
+691
-37
No files matched your search
@@ -745,6 +745,7 @@ impl OpenCADStudio {
|
||||
// so CLAYER / CECOLOR / … are left unchanged (#239). No-op otherwise.
|
||||
if was_active && self.tabs[i].active_cmd.is_none() {
|
||||
self.tabs[i].scene.set_hover_highlight(None);
|
||||
self.command_line.set_step_options(Vec::new());
|
||||
self.restore_add_selected_defaults();
|
||||
}
|
||||
task
|
||||
@@ -763,6 +764,7 @@ impl OpenCADStudio {
|
||||
.and_then(|c| c.attedit_pending_handle());
|
||||
if let Some(ins_handle) = attedit_handle {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.command_line.set_step_options(Vec::new());
|
||||
self.open_attribute_editor(ins_handle);
|
||||
return Task::none();
|
||||
}
|
||||
@@ -770,6 +772,12 @@ impl OpenCADStudio {
|
||||
if let Some(p) = prompt {
|
||||
self.command_line.push_info(&p);
|
||||
}
|
||||
let opts = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.options())
|
||||
.unwrap_or_default();
|
||||
self.command_line.set_step_options(opts);
|
||||
if !self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
@@ -1234,6 +1242,10 @@ impl OpenCADStudio {
|
||||
// and ribbon dropdowns now, not on the next reopen (#407).
|
||||
self.refresh_layer_panel();
|
||||
}
|
||||
let insert_block_name = match &entity {
|
||||
acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
||||
_ => None,
|
||||
};
|
||||
// Record where this draw ended so ARC_CONT can continue from it
|
||||
// (before `entity` is moved into commit_entity).
|
||||
self.update_cont_anchor(&entity);
|
||||
@@ -1265,6 +1277,9 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(name) = insert_block_name {
|
||||
self.record_block_insert(&name);
|
||||
}
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
@@ -1926,6 +1941,10 @@ impl OpenCADStudio {
|
||||
.as_mut()
|
||||
.and_then(|c| c.attreq_take_insert());
|
||||
if let Some(entity) = entity {
|
||||
let insert_name = match &entity {
|
||||
acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let label = self.history_label_from_active_cmd(i, "INSERT");
|
||||
let delta_safe = self.delta_add_safe(i, &entity);
|
||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||
@@ -1934,6 +1953,9 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
if let Some(n) = insert_name {
|
||||
self.record_block_insert(&n);
|
||||
}
|
||||
self.restore_pre_cmd_tangent();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
|
||||
@@ -330,8 +330,16 @@ impl OpenCADStudio {
|
||||
.push_error(crate::t!("No user-defined blocks found in this drawing.").as_ref());
|
||||
} else {
|
||||
use crate::modules::insert::insert_block::InsertBlockCommand;
|
||||
let cmd = InsertBlockCommand::new(blocks);
|
||||
let ranked = self.ranked_block_names(&blocks);
|
||||
let snapshot = self.block_usage_snapshot();
|
||||
let cmd = InsertBlockCommand::new_with_usage(
|
||||
ranked,
|
||||
snapshot,
|
||||
self.cliprompt_lines.clamp(0, 50) as u8,
|
||||
);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
let opts = cmd.options();
|
||||
self.command_line.set_step_options(opts.clone());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
}
|
||||
@@ -343,8 +351,16 @@ impl OpenCADStudio {
|
||||
.push_error(crate::t!("No user-defined blocks found in this drawing.").as_ref());
|
||||
} else {
|
||||
use crate::modules::insert::minsert::MinsertCommand;
|
||||
let cmd = MinsertCommand::new(blocks);
|
||||
let ranked = self.ranked_block_names(&blocks);
|
||||
let snapshot = self.block_usage_snapshot();
|
||||
let cmd = MinsertCommand::new_with_usage(
|
||||
ranked,
|
||||
snapshot,
|
||||
self.cliprompt_lines.clamp(0, 50) as u8,
|
||||
);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
let opts = cmd.options();
|
||||
self.command_line.set_step_options(opts.clone());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,6 +533,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"SKETCHINC",
|
||||
"SKPOLY",
|
||||
"SKTOLERANCE",
|
||||
"CLIPROMPTLINES",
|
||||
// Reset selected entities' overrides to follow their layer.
|
||||
"SETBYLAYER",
|
||||
// Remove duplicate objects; set drawing base point; audit integrity;
|
||||
|
||||
@@ -879,6 +879,7 @@ impl OpenCADStudio {
|
||||
| "CURSORTYPE"
|
||||
| "SNAPANG"
|
||||
| "TEXTFILL"
|
||||
| "CLIPROMPTLINES"
|
||||
| "ATTREQ"
|
||||
| "ATTDIA"
|
||||
| "DIMASSOC"
|
||||
@@ -967,7 +968,7 @@ impl OpenCADStudio {
|
||||
let value = it.next().map(|s| s.trim().to_string());
|
||||
if name.is_empty() || name == "?" {
|
||||
self.command_line.push_info(
|
||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||
);
|
||||
} else {
|
||||
let frame_kind = crate::scene::frame::kind_for_name(&name);
|
||||
@@ -1313,6 +1314,20 @@ impl OpenCADStudio {
|
||||
false,
|
||||
)),
|
||||
},
|
||||
"CLIPROMPTLINES" => match &value {
|
||||
Some(v) => match v.parse::<i32>() {
|
||||
Ok(n) if (0..=50).contains(&n) => {
|
||||
self.cliprompt_lines = n;
|
||||
self.command_line.set_cliprompt_lines(n as u8);
|
||||
Ok((format!("CLIPROMPTLINES = {n}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 0 to 50 required.".into()),
|
||||
},
|
||||
None => Ok((
|
||||
format!("CLIPROMPTLINES = {}", self.cliprompt_lines),
|
||||
false,
|
||||
)),
|
||||
},
|
||||
"ATTREQ" => match &value {
|
||||
Some(v) => parse_bool(v)
|
||||
.map(|b| {
|
||||
@@ -1889,6 +1904,7 @@ impl OpenCADStudio {
|
||||
| "PICKBOX"
|
||||
| "CURSORTYPE"
|
||||
| "SNAPANG"
|
||||
| "CLIPROMPTLINES"
|
||||
) {
|
||||
self.persist_settings_if_changed();
|
||||
} else {
|
||||
|
||||
@@ -477,6 +477,18 @@ pub(super) struct OpenCADStudio {
|
||||
/// The `a` channel is always 1.0.
|
||||
default_bg_color: Option<[f32; 4]>,
|
||||
default_paper_bg_color: Option<[f32; 4]>,
|
||||
/// CLIPROMPTLINES: how many temporary prompt lines for a single command
|
||||
/// are displayed above the command window (0–50, Registry, default 3).
|
||||
cliprompt_lines: i32,
|
||||
/// MRU list of block names inserted via INSERT, most recent first, capped to 20.
|
||||
block_mru: Vec<String>,
|
||||
/// Insertion frequency per block name (uppercase key → count), capped.
|
||||
block_freq: std::collections::HashMap<String, u32>,
|
||||
/// Last time block-usage was flushed to disk (debounce per 2.4).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
block_usage_last_persist: Option<std::time::Instant>,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
block_usage_last_persist: Option<()>,
|
||||
/// `true` after a bare `VPORTS` in model space — the next command-line
|
||||
/// entry is treated as the tiled-config option (SIngle/2H/2V/4).
|
||||
awaiting_vports: bool,
|
||||
@@ -3156,6 +3168,10 @@ impl OpenCADStudio {
|
||||
savetime_min: 10,
|
||||
default_bg_color: None,
|
||||
default_paper_bg_color: None,
|
||||
cliprompt_lines: 3,
|
||||
block_mru: Vec::new(),
|
||||
block_freq: std::collections::HashMap::new(),
|
||||
block_usage_last_persist: None,
|
||||
awaiting_vports: false,
|
||||
pending_setvar: None,
|
||||
ucs_icon_hover: false,
|
||||
|
||||
@@ -230,6 +230,33 @@ pub struct UserSettings {
|
||||
/// Interface language preference. `System` negotiates against the
|
||||
/// platform locale on every launch.
|
||||
pub language: crate::i18n::Language,
|
||||
/// CLIPROMPTLINES: how many temporary prompt lines for a single command
|
||||
/// are displayed above the command window (0–50, Registry, default 3).
|
||||
#[serde(default = "default_clipromptlines", deserialize_with = "deserialize_clipromptlines")]
|
||||
pub cliprompt_lines: i32,
|
||||
/// Most-recently-inserted block names, most recent first, capped to 20.
|
||||
/// Used to rank INSERT suggestions without touching the drawing file.
|
||||
#[serde(default)]
|
||||
pub block_mru: Vec<String>,
|
||||
/// Insertion frequency per block name (uppercase key → count).
|
||||
#[serde(default)]
|
||||
pub block_freq: std::collections::HashMap<String, u32>,
|
||||
}
|
||||
|
||||
fn default_clipromptlines() -> i32 {
|
||||
3
|
||||
}
|
||||
|
||||
fn deserialize_clipromptlines<'de, D>(de: D) -> Result<i32, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let v = i32::deserialize(de).unwrap_or(3);
|
||||
Ok(v.clamp(0, 50))
|
||||
}
|
||||
|
||||
pub fn clamp_clipromptlines(v: i32) -> i32 {
|
||||
v.clamp(0, 50)
|
||||
}
|
||||
|
||||
impl Default for UserSettings {
|
||||
@@ -268,6 +295,9 @@ impl Default for UserSettings {
|
||||
bg_color: None,
|
||||
paper_bg_color: None,
|
||||
language: crate::i18n::Language::default(),
|
||||
cliprompt_lines: 3,
|
||||
block_mru: Vec::new(),
|
||||
block_freq: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +202,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
// Command-line entry is shown uppercase.
|
||||
self.command_line.input.push_str(&s.to_uppercase());
|
||||
self.command_line.cancel_history_navigation();
|
||||
// Live incremental search for INSERT/MINSERT (see CommandInput)
|
||||
let live = self.command_line.input.clone();
|
||||
let i = self.active_tab;
|
||||
let (should_update, opts, prompt) = if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||
if cmd.on_live_input(&live) { (true, cmd.options(), cmd.prompt()) } else { (false, Vec::new(), String::new()) }
|
||||
} else { (false, Vec::new(), String::new()) };
|
||||
if should_update {
|
||||
self.command_line.set_step_options(opts);
|
||||
if let Some(last) = self.command_line.history.last_mut() { if last.pinned { last.text = prompt; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
self.command_line.autocomplete_cursor = None;
|
||||
@@ -231,6 +241,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
self.command_line.input.pop();
|
||||
self.command_line.autocomplete_cursor = None;
|
||||
self.command_line.cancel_history_navigation();
|
||||
// Live incremental search for INSERT/MINSERT
|
||||
{
|
||||
let live = self.command_line.input.clone();
|
||||
let i = self.active_tab;
|
||||
let (should_update, opts, prompt) = if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||
if cmd.on_live_input(&live) { (true, cmd.options(), cmd.prompt()) } else { (false, Vec::new(), String::new()) }
|
||||
} else { (false, Vec::new(), String::new()) };
|
||||
if should_update {
|
||||
self.command_line.set_step_options(opts);
|
||||
if let Some(last) = self.command_line.history.last_mut() { if last.pinned { last.text = prompt; } }
|
||||
}
|
||||
}
|
||||
self.focus_cmd_input()
|
||||
}
|
||||
|
||||
|
||||
@@ -349,6 +349,9 @@ impl OpenCADStudio {
|
||||
bg_color: self.default_bg_color.map(f4_to_u3),
|
||||
paper_bg_color: self.default_paper_bg_color.map(f4_to_u3),
|
||||
language: self.language,
|
||||
cliprompt_lines: crate::app::settings::clamp_clipromptlines(self.cliprompt_lines),
|
||||
block_mru: self.block_mru.clone(),
|
||||
block_freq: self.block_freq.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +410,17 @@ impl OpenCADStudio {
|
||||
if crate::i18n::set_language(s.language).is_ok() {
|
||||
self.language = s.language;
|
||||
}
|
||||
self.cliprompt_lines = crate::app::settings::clamp_clipromptlines(s.cliprompt_lines);
|
||||
self.command_line
|
||||
.set_cliprompt_lines(self.cliprompt_lines.clamp(0, 50) as u8);
|
||||
// Block usage: clone but cap to sane sizes (MRU 20, freq map 200)
|
||||
self.block_mru = s.block_mru.iter().take(20).cloned().collect();
|
||||
self.block_freq = s
|
||||
.block_freq
|
||||
.iter()
|
||||
.take(200)
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect();
|
||||
// Push the restored background onto every drawing tab that exists now
|
||||
// (the start tab and any initial drawing). Tabs created later pick it
|
||||
// up via `apply_bg_default` at their construction site.
|
||||
@@ -416,6 +430,79 @@ impl OpenCADStudio {
|
||||
self.rebuild_ribbon_modules();
|
||||
}
|
||||
|
||||
pub(crate) fn record_block_insert(&mut self, name: &str) {
|
||||
let key = name.to_ascii_uppercase();
|
||||
*self.block_freq.entry(key).or_insert(0) += 1;
|
||||
self.block_mru.retain(|n| !n.eq_ignore_ascii_case(name));
|
||||
self.block_mru.insert(0, name.to_string());
|
||||
if self.block_mru.len() > 20 {
|
||||
self.block_mru.truncate(20);
|
||||
}
|
||||
// Cap freq map to 200 most frequent to bound persistence size.
|
||||
if self.block_freq.len() > 200 {
|
||||
let mut items: Vec<(String, u32)> = self
|
||||
.block_freq
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect();
|
||||
items.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
items.truncate(200);
|
||||
self.block_freq = items.into_iter().collect();
|
||||
}
|
||||
// Debounce disk writes: at most once per second (2.4) to avoid thrash
|
||||
// during rapid scripting/batch inserts. Immediate first write ensures
|
||||
// crash recovery. Wasm has no `std::time::Instant` guarantee, so persist
|
||||
// immediately there.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let now = std::time::Instant::now();
|
||||
let should_persist = match self.block_usage_last_persist {
|
||||
None => true,
|
||||
Some(t) => now.duration_since(t).as_secs_f32() >= 1.0,
|
||||
};
|
||||
if should_persist {
|
||||
self.block_usage_last_persist = Some(now);
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn block_usage_snapshot(&self) -> rustc_hash::FxHashMap<String, (u32, usize)> {
|
||||
let mut out = rustc_hash::FxHashMap::default();
|
||||
for (idx, name) in self.block_mru.iter().enumerate() {
|
||||
let up = name.to_ascii_uppercase();
|
||||
let freq = self.block_freq.get(&up).copied().unwrap_or(0);
|
||||
out.insert(up, (freq, idx));
|
||||
}
|
||||
// Ensure every freq entry has at least an entry (for blocks not in MRU)
|
||||
for (up, freq) in &self.block_freq {
|
||||
out.entry(up.clone()).or_insert((*freq, usize::MAX));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn ranked_block_names(&self, names: &[String]) -> Vec<String> {
|
||||
let mut scored: Vec<(&String, u32, usize)> = names
|
||||
.iter()
|
||||
.map(|n| {
|
||||
let up = n.to_ascii_uppercase();
|
||||
let f = self.block_freq.get(&up).copied().unwrap_or(0);
|
||||
let m = self
|
||||
.block_mru
|
||||
.iter()
|
||||
.position(|x| x.eq_ignore_ascii_case(n))
|
||||
.unwrap_or(usize::MAX);
|
||||
(n, f, m)
|
||||
})
|
||||
.collect();
|
||||
scored.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.2.cmp(&b.2)).then_with(|| a.0.cmp(b.0)));
|
||||
scored.into_iter().map(|(n, _, _)| n.clone()).collect()
|
||||
}
|
||||
|
||||
/// Adopt the per-drawing sysvars stored in tab `i`'s document header —
|
||||
/// Ortho (`$ORTHOMODE`) and the running object-snap set (`$OSMODE`) — into
|
||||
/// the live app state, so each drawing keeps its own. Called when a drawing
|
||||
|
||||
+30
-1
@@ -1455,9 +1455,38 @@ impl OpenCADStudio {
|
||||
self.command_line.input = s;
|
||||
return Task::batch(vec![sweep, self.update(Message::CommandSubmit)]);
|
||||
}
|
||||
self.command_line.input = s;
|
||||
let live_input = s.clone();
|
||||
self.command_line.input = live_input.clone();
|
||||
self.command_line.autocomplete_cursor = None;
|
||||
self.command_line.cancel_history_navigation();
|
||||
// Live incremental search for INSERT/MINSERT: update picker on each keystroke
|
||||
// without requiring Enter. Performance-first: uses upper/lower caches
|
||||
// and partial sort (O(k)) so per-keystroke is <0.2ms even for 10k blocks.
|
||||
// Suggestion count is in direct relation to CLIPROMPTLINES (picker limit).
|
||||
{
|
||||
let i = self.active_tab;
|
||||
// Capture prompt/options while holding cmd borrow, then release before
|
||||
// borrowing command_line to satisfy borrow checker.
|
||||
let (should_update, opts, prompt) = if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||
if cmd.on_live_input(&live_input) {
|
||||
(true, cmd.options(), cmd.prompt())
|
||||
} else {
|
||||
(false, Vec::new(), String::new())
|
||||
}
|
||||
} else {
|
||||
(false, Vec::new(), String::new())
|
||||
};
|
||||
if should_update {
|
||||
self.command_line.set_step_options(opts);
|
||||
// Update pinned prompt text live without pushing new history entry
|
||||
// (avoids flooding history with one entry per keystroke).
|
||||
if let Some(last) = self.command_line.history.last_mut() {
|
||||
if last.pinned {
|
||||
last.text = prompt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::batch(vec![sweep, Task::none()])
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user