forked from 1b-agent/opencadstudio-upstream
feat(purge): nested fixpoint purge + option prompt (#375)
PURGE now repeats until a pass removes nothing, so definitions freed by a purged block (nested blocks, layers, linetypes, styles) go in one run. Reference scan keeps layer linetypes, ATTDEF/ATTRIB text styles, and dimstyle DIMTXSTY + arrow blocks; the undo snapshot moves before the first removal so one UNDO restores everything. Bare PURGE prompts with clickable category options (aliases B/LA/LT/ST run directly), and pinned prompts drop their "[A / B / …]" listing when the same options render as buttons. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
5ba8928675
commit
3205f7a2b9
2 files changed
+266
-143
No files matched your search
+244
-141
@@ -147,149 +147,247 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
// ── PURGE unused definitions ──────────────────────────────────
|
||||
cmd if cmd == "PURGE" || cmd.starts_with("PURGE ") => {
|
||||
// Bare PURGE prompts with the category options as clickable
|
||||
// keywords; PURGE <sub> (typed, clicked or scripted) runs directly.
|
||||
"PURGE" => {
|
||||
use crate::command::KeywordCommand;
|
||||
let c = KeywordCommand::new(
|
||||
"PURGE",
|
||||
"PURGE [All / Blocks / LAyers / LineTypes / STyles]:",
|
||||
vec![
|
||||
("All", "ALL", None),
|
||||
("Blocks", "BLOCKS", None),
|
||||
("LAyers", "LAYERS", None),
|
||||
("LineTypes", "LINETYPES", None),
|
||||
("STyles", "STYLES", None),
|
||||
],
|
||||
);
|
||||
self.command_line.push_info(&c.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
}
|
||||
cmd if cmd.starts_with("PURGE ") => {
|
||||
let sub = cmd
|
||||
.split_whitespace()
|
||||
.nth(1)
|
||||
.unwrap_or("ALL")
|
||||
.to_uppercase();
|
||||
let all = sub == "ALL" || sub.is_empty();
|
||||
let do_layers = all || matches!(sub.as_str(), "LAYERS" | "LAYER" | "LA");
|
||||
let do_styles = all || matches!(sub.as_str(), "TEXTSTYLES" | "STYLES" | "ST");
|
||||
let do_lts = all || matches!(sub.as_str(), "LINETYPES" | "LT");
|
||||
let do_blocks = all || matches!(sub.as_str(), "BLOCKS" | "BLOCK" | "B");
|
||||
if !(do_layers || do_styles || do_lts || do_blocks) {
|
||||
self.command_line.push_error(&format!(
|
||||
"PURGE: unknown option \"{sub}\" — use ALL, Blocks, LAyers, LineTypes or STyles."
|
||||
));
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
// Collect names in use (immutable borrows — done in their own scope)
|
||||
let used_layers: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|e| {
|
||||
let name = &e.common().layer;
|
||||
if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.clone())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let used_text_styles: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
acadrust::EntityType::Text(t) => Some(t.style.clone()),
|
||||
acadrust::EntityType::MText(t) => Some(t.style.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
let used_linetypes: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|e| {
|
||||
let lt = &e.common().linetype;
|
||||
if lt.is_empty() || lt == "ByLayer" || lt == "ByBlock" {
|
||||
None
|
||||
} else {
|
||||
Some(lt.clone())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let used_blocks: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
||||
acadrust::EntityType::Dimension(d) => Some(d.base().block_name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
// Build removal lists (still immutable)
|
||||
let layer_remove: Vec<String> = if all || sub == "LAYERS" {
|
||||
self.tabs[i]
|
||||
// Purging a block frees whatever only its members referenced
|
||||
// (nested blocks, layers, linetypes, styles), so those become
|
||||
// purgeable on the next pass — repeat until a pass removes
|
||||
// nothing, so one PURGE does what re-running it used to.
|
||||
let (mut n_layers, mut n_styles, mut n_lts, mut n_blocks) =
|
||||
(0usize, 0usize, 0usize, 0usize);
|
||||
let mut snapshot_pushed = false;
|
||||
loop {
|
||||
// Collect names in use (immutable borrows — own scope per pass).
|
||||
let used_layers: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|l| l.name != "0" && !used_layers.contains(&l.name))
|
||||
.map(|l| l.name.clone())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let style_remove: Vec<String> = if all || sub == "TEXTSTYLES" || sub == "STYLES" {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.iter()
|
||||
.filter(|s| s.name != "Standard" && !used_text_styles.contains(&s.name))
|
||||
.map(|s| s.name.clone())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let lt_remove: Vec<String> = if all || sub == "LINETYPES" || sub == "LT" {
|
||||
let standard = ["Continuous", "ByLayer", "ByBlock"];
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.line_types
|
||||
.iter()
|
||||
.filter(|lt| {
|
||||
!standard.iter().any(|s| s.eq_ignore_ascii_case(<.name))
|
||||
&& !used_linetypes.contains(<.name)
|
||||
.entities()
|
||||
.filter_map(|e| {
|
||||
let name = &e.common().layer;
|
||||
if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.clone())
|
||||
}
|
||||
})
|
||||
.map(|lt| lt.name.clone())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Blocks: definitions with no INSERT/Dimension reference.
|
||||
// Skip anonymous (*Model_Space/*Paper_Space/*U*/*D*…), layout,
|
||||
// and xref blocks — those are system- or reference-managed.
|
||||
let block_remove: Vec<String> = if all || sub == "BLOCKS" {
|
||||
self.tabs[i]
|
||||
.collect();
|
||||
// Text styles: entity references plus every dimension
|
||||
// style's DIMTXSTY — a style only a dimstyle uses is not
|
||||
// purgeable.
|
||||
let used_text_styles: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|br| {
|
||||
!br.is_anonymous()
|
||||
&& !br.is_layout()
|
||||
&& !br.flags.is_xref
|
||||
&& !br.flags.is_xref_overlay
|
||||
&& !used_blocks.contains(&br.name)
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
acadrust::EntityType::Text(t) => Some(t.style.clone()),
|
||||
acadrust::EntityType::MText(t) => Some(t.style.clone()),
|
||||
acadrust::EntityType::AttributeDefinition(a) => {
|
||||
Some(a.text_style.clone())
|
||||
}
|
||||
acadrust::EntityType::AttributeEntity(a) => {
|
||||
Some(a.text_style.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.map(|br| br.name.clone())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
.chain(
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.map(|ds| ds.dimtxsty.clone()),
|
||||
)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
// Linetypes: entity references plus every layer's own
|
||||
// linetype — a ByLayer entity reaches it through the layer.
|
||||
let used_linetypes: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|e| {
|
||||
let lt = &e.common().linetype;
|
||||
if lt.is_empty() || lt == "ByLayer" || lt == "ByBlock" {
|
||||
None
|
||||
} else {
|
||||
Some(lt.clone())
|
||||
}
|
||||
})
|
||||
.chain(
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|l| l.line_type.clone()),
|
||||
)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
// Apply removals (mutable)
|
||||
let purged = layer_remove.len()
|
||||
+ style_remove.len()
|
||||
+ lt_remove.len()
|
||||
+ block_remove.len();
|
||||
for name in &layer_remove {
|
||||
self.tabs[i].scene.document.layers.remove(name);
|
||||
}
|
||||
for name in &style_remove {
|
||||
self.tabs[i].scene.document.text_styles.remove(name);
|
||||
}
|
||||
for name in <_remove {
|
||||
self.tabs[i].scene.document.line_types.remove(name);
|
||||
}
|
||||
for name in &block_remove {
|
||||
// Drop the block definition's member entities (and the
|
||||
// BLOCK/ENDBLK delimiters) before the record so no orphaned
|
||||
// geometry survives in the document entity list.
|
||||
let handles: Vec<_> =
|
||||
if let Some(br) = self.tabs[i].scene.document.block_records.get(name) {
|
||||
let mut used_blocks: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
||||
acadrust::EntityType::Dimension(d) => {
|
||||
Some(d.base().block_name.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
// Dimension-style arrowheads reference blocks by handle
|
||||
// (DIMBLK/DIMBLK1/DIMBLK2/DIMLDRBLK) — keep those too.
|
||||
{
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let arrow_handles: Vec<acadrust::Handle> = doc
|
||||
.dim_styles
|
||||
.iter()
|
||||
.flat_map(|ds| [ds.dimblk, ds.dimblk1, ds.dimblk2, ds.dimldrblk])
|
||||
.filter(|h| !h.is_null())
|
||||
.collect();
|
||||
if !arrow_handles.is_empty() {
|
||||
for br in doc.block_records.iter() {
|
||||
if arrow_handles.contains(&br.handle) {
|
||||
used_blocks.insert(br.name.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build removal lists (still immutable)
|
||||
let layer_remove: Vec<String> = if do_layers {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.filter(|l| l.name != "0" && !used_layers.contains(&l.name))
|
||||
.map(|l| l.name.clone())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let style_remove: Vec<String> = if do_styles {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.iter()
|
||||
.filter(|s| {
|
||||
s.name != "Standard" && !used_text_styles.contains(&s.name)
|
||||
})
|
||||
.map(|s| s.name.clone())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let lt_remove: Vec<String> = if do_lts {
|
||||
let standard = ["Continuous", "ByLayer", "ByBlock"];
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.line_types
|
||||
.iter()
|
||||
.filter(|lt| {
|
||||
!standard.iter().any(|s| s.eq_ignore_ascii_case(<.name))
|
||||
&& !used_linetypes.contains(<.name)
|
||||
})
|
||||
.map(|lt| lt.name.clone())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Blocks: definitions with no INSERT/Dimension reference.
|
||||
// Skip anonymous (*Model_Space/*Paper_Space/*U*/*D*…), layout,
|
||||
// and xref blocks — those are system- or reference-managed.
|
||||
let block_remove: Vec<String> = if do_blocks {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|br| {
|
||||
!br.is_anonymous()
|
||||
&& !br.is_layout()
|
||||
&& !br.flags.is_xref
|
||||
&& !br.flags.is_xref_overlay
|
||||
&& !used_blocks.contains(&br.name)
|
||||
})
|
||||
.map(|br| br.name.clone())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
if layer_remove.len()
|
||||
+ style_remove.len()
|
||||
+ lt_remove.len()
|
||||
+ block_remove.len()
|
||||
== 0
|
||||
{
|
||||
break;
|
||||
}
|
||||
// Snapshot the pre-purge document once, before the first
|
||||
// removal, so a single UNDO restores everything.
|
||||
if !snapshot_pushed {
|
||||
self.push_undo_snapshot(i, "PURGE");
|
||||
snapshot_pushed = true;
|
||||
}
|
||||
|
||||
// Apply removals (mutable)
|
||||
for name in &layer_remove {
|
||||
self.tabs[i].scene.document.layers.remove(name);
|
||||
}
|
||||
for name in &style_remove {
|
||||
self.tabs[i].scene.document.text_styles.remove(name);
|
||||
}
|
||||
for name in <_remove {
|
||||
self.tabs[i].scene.document.line_types.remove(name);
|
||||
}
|
||||
for name in &block_remove {
|
||||
// Drop the block definition's member entities (and the
|
||||
// BLOCK/ENDBLK delimiters) before the record so no orphaned
|
||||
// geometry survives in the document entity list.
|
||||
let handles: Vec<_> = if let Some(br) =
|
||||
self.tabs[i].scene.document.block_records.get(name)
|
||||
{
|
||||
let mut h = br.entity_handles.clone();
|
||||
h.push(br.block_entity_handle);
|
||||
h.push(br.block_end_handle);
|
||||
@@ -297,31 +395,36 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
for h in handles {
|
||||
self.tabs[i].scene.document.remove_entity(h);
|
||||
for h in handles {
|
||||
self.tabs[i].scene.document.remove_entity(h);
|
||||
}
|
||||
self.tabs[i].scene.document.block_records.remove(name);
|
||||
}
|
||||
self.tabs[i].scene.document.block_records.remove(name);
|
||||
n_layers += layer_remove.len();
|
||||
n_styles += style_remove.len();
|
||||
n_lts += lt_remove.len();
|
||||
n_blocks += block_remove.len();
|
||||
}
|
||||
|
||||
let purged = n_layers + n_styles + n_lts + n_blocks;
|
||||
if purged > 0 {
|
||||
self.push_undo_snapshot(i, "PURGE");
|
||||
self.tabs[i].dirty = true;
|
||||
// Rebuild the layer/style/linetype panel + ribbon caches so
|
||||
// the removed definitions disappear from the UI immediately.
|
||||
self.refresh_layer_panel();
|
||||
// Per-type breakdown so the user sees exactly what went.
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
if !layer_remove.is_empty() {
|
||||
parts.push(format!("{} layer(s)", layer_remove.len()));
|
||||
if n_layers > 0 {
|
||||
parts.push(format!("{n_layers} layer(s)"));
|
||||
}
|
||||
if !style_remove.is_empty() {
|
||||
parts.push(format!("{} text style(s)", style_remove.len()));
|
||||
if n_styles > 0 {
|
||||
parts.push(format!("{n_styles} text style(s)"));
|
||||
}
|
||||
if !lt_remove.is_empty() {
|
||||
parts.push(format!("{} linetype(s)", lt_remove.len()));
|
||||
if n_lts > 0 {
|
||||
parts.push(format!("{n_lts} linetype(s)"));
|
||||
}
|
||||
if !block_remove.is_empty() {
|
||||
parts.push(format!("{} block(s)", block_remove.len()));
|
||||
if n_blocks > 0 {
|
||||
parts.push(format!("{n_blocks} block(s)"));
|
||||
}
|
||||
self.command_line.push_output(&format!(
|
||||
"PURGE: {} definition(s) removed — {}.",
|
||||
|
||||
+22
-2
@@ -22,6 +22,23 @@ fn cmd_input_id() -> iced::widget::Id {
|
||||
iced::widget::Id::new(CMD_INPUT_ID)
|
||||
}
|
||||
|
||||
/// Drop a prompt's "[A / B / …]" option listing — used for the pinned line
|
||||
/// when the same options render as clickable buttons beside it. Collapses the
|
||||
/// leftover double spaces and the orphaned " :" the removal leaves behind.
|
||||
fn strip_option_listing(s: &str) -> String {
|
||||
let Some(a) = s.find('[') else {
|
||||
return s.to_string();
|
||||
};
|
||||
let Some(off) = s[a..].find(']') else {
|
||||
return s.to_string();
|
||||
};
|
||||
let mut out = format!("{}{}", &s[..a], &s[a + off + 1..]);
|
||||
while out.contains(" ") {
|
||||
out = out.replace(" ", " ");
|
||||
}
|
||||
out.replace(" :", ":").trim().to_string()
|
||||
}
|
||||
|
||||
const MAX_HISTORY: usize = 64;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -346,9 +363,12 @@ impl CommandLine {
|
||||
// The current step's prompt is the single pinned line. When the
|
||||
// step offers options, render them as clickable buttons inline
|
||||
// beside the prompt text — so options need never be typed and
|
||||
// don't clutter the prompt string itself. (#304)
|
||||
// don't clutter the prompt string itself. (#304) The buttons
|
||||
// already name every choice, so the prompt's own "[A / B / …]"
|
||||
// listing is dropped here (the history log keeps the full text).
|
||||
if entry.pinned && !self.step_options.is_empty() {
|
||||
let mut r = row![text(&entry.text).size(11).color(color)]
|
||||
let shown = strip_option_listing(&entry.text);
|
||||
let mut r = row![text(shown).size(11).color(color)]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center);
|
||||
for opt in &self.step_options {
|
||||
|
||||
Reference in new issue
Block a user