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 <noreply@anthropic.com>
This commit is contained in:
parent
5ba8928675
commit
3205f7a2b9
2 changed files with 261 additions and 138 deletions
|
|
@ -147,149 +147,247 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── PURGE unused definitions ──────────────────────────────────
|
// ── 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
|
let sub = cmd
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.nth(1)
|
.nth(1)
|
||||||
.unwrap_or("ALL")
|
.unwrap_or("ALL")
|
||||||
.to_uppercase();
|
.to_uppercase();
|
||||||
let all = sub == "ALL" || sub.is_empty();
|
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)
|
// Purging a block frees whatever only its members referenced
|
||||||
let used_layers: rustc_hash::FxHashSet<String> = self.tabs[i]
|
// (nested blocks, layers, linetypes, styles), so those become
|
||||||
.scene
|
// purgeable on the next pass — repeat until a pass removes
|
||||||
.document
|
// nothing, so one PURGE does what re-running it used to.
|
||||||
.entities()
|
let (mut n_layers, mut n_styles, mut n_lts, mut n_blocks) =
|
||||||
.filter_map(|e| {
|
(0usize, 0usize, 0usize, 0usize);
|
||||||
let name = &e.common().layer;
|
let mut snapshot_pushed = false;
|
||||||
if name.is_empty() {
|
loop {
|
||||||
None
|
// Collect names in use (immutable borrows — own scope per pass).
|
||||||
} else {
|
let used_layers: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||||
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]
|
|
||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
.layers
|
.entities()
|
||||||
.iter()
|
.filter_map(|e| {
|
||||||
.filter(|l| l.name != "0" && !used_layers.contains(&l.name))
|
let name = &e.common().layer;
|
||||||
.map(|l| l.name.clone())
|
if name.is_empty() {
|
||||||
.collect()
|
None
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
Some(name.clone())
|
||||||
};
|
}
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
.map(|lt| lt.name.clone())
|
.collect();
|
||||||
.collect()
|
// Text styles: entity references plus every dimension
|
||||||
} else {
|
// style's DIMTXSTY — a style only a dimstyle uses is not
|
||||||
vec![]
|
// purgeable.
|
||||||
};
|
let used_text_styles: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||||
|
|
||||||
// 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]
|
|
||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
.block_records
|
.entities()
|
||||||
.iter()
|
.filter_map(|e| match e {
|
||||||
.filter(|br| {
|
acadrust::EntityType::Text(t) => Some(t.style.clone()),
|
||||||
!br.is_anonymous()
|
acadrust::EntityType::MText(t) => Some(t.style.clone()),
|
||||||
&& !br.is_layout()
|
acadrust::EntityType::AttributeDefinition(a) => {
|
||||||
&& !br.flags.is_xref
|
Some(a.text_style.clone())
|
||||||
&& !br.flags.is_xref_overlay
|
}
|
||||||
&& !used_blocks.contains(&br.name)
|
acadrust::EntityType::AttributeEntity(a) => {
|
||||||
|
Some(a.text_style.clone())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
})
|
})
|
||||||
.map(|br| br.name.clone())
|
.chain(
|
||||||
.collect()
|
self.tabs[i]
|
||||||
} else {
|
.scene
|
||||||
vec![]
|
.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 mut used_blocks: rustc_hash::FxHashSet<String> = self.tabs[i]
|
||||||
let purged = layer_remove.len()
|
.scene
|
||||||
+ style_remove.len()
|
.document
|
||||||
+ lt_remove.len()
|
.entities()
|
||||||
+ block_remove.len();
|
.filter_map(|e| match e {
|
||||||
for name in &layer_remove {
|
acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
||||||
self.tabs[i].scene.document.layers.remove(name);
|
acadrust::EntityType::Dimension(d) => {
|
||||||
}
|
Some(d.base().block_name.clone())
|
||||||
for name in &style_remove {
|
}
|
||||||
self.tabs[i].scene.document.text_styles.remove(name);
|
_ => None,
|
||||||
}
|
})
|
||||||
for name in <_remove {
|
.filter(|s| !s.is_empty())
|
||||||
self.tabs[i].scene.document.line_types.remove(name);
|
.collect();
|
||||||
}
|
// Dimension-style arrowheads reference blocks by handle
|
||||||
for name in &block_remove {
|
// (DIMBLK/DIMBLK1/DIMBLK2/DIMLDRBLK) — keep those too.
|
||||||
// Drop the block definition's member entities (and the
|
{
|
||||||
// BLOCK/ENDBLK delimiters) before the record so no orphaned
|
let doc = &self.tabs[i].scene.document;
|
||||||
// geometry survives in the document entity list.
|
let arrow_handles: Vec<acadrust::Handle> = doc
|
||||||
let handles: Vec<_> =
|
.dim_styles
|
||||||
if let Some(br) = self.tabs[i].scene.document.block_records.get(name) {
|
.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();
|
let mut h = br.entity_handles.clone();
|
||||||
h.push(br.block_entity_handle);
|
h.push(br.block_entity_handle);
|
||||||
h.push(br.block_end_handle);
|
h.push(br.block_end_handle);
|
||||||
|
|
@ -297,31 +395,36 @@ impl OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
for h in handles {
|
for h in handles {
|
||||||
self.tabs[i].scene.document.remove_entity(h);
|
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 {
|
if purged > 0 {
|
||||||
self.push_undo_snapshot(i, "PURGE");
|
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
// Rebuild the layer/style/linetype panel + ribbon caches so
|
// Rebuild the layer/style/linetype panel + ribbon caches so
|
||||||
// the removed definitions disappear from the UI immediately.
|
// the removed definitions disappear from the UI immediately.
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
// Per-type breakdown so the user sees exactly what went.
|
// Per-type breakdown so the user sees exactly what went.
|
||||||
let mut parts: Vec<String> = Vec::new();
|
let mut parts: Vec<String> = Vec::new();
|
||||||
if !layer_remove.is_empty() {
|
if n_layers > 0 {
|
||||||
parts.push(format!("{} layer(s)", layer_remove.len()));
|
parts.push(format!("{n_layers} layer(s)"));
|
||||||
}
|
}
|
||||||
if !style_remove.is_empty() {
|
if n_styles > 0 {
|
||||||
parts.push(format!("{} text style(s)", style_remove.len()));
|
parts.push(format!("{n_styles} text style(s)"));
|
||||||
}
|
}
|
||||||
if !lt_remove.is_empty() {
|
if n_lts > 0 {
|
||||||
parts.push(format!("{} linetype(s)", lt_remove.len()));
|
parts.push(format!("{n_lts} linetype(s)"));
|
||||||
}
|
}
|
||||||
if !block_remove.is_empty() {
|
if n_blocks > 0 {
|
||||||
parts.push(format!("{} block(s)", block_remove.len()));
|
parts.push(format!("{n_blocks} block(s)"));
|
||||||
}
|
}
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"PURGE: {} definition(s) removed — {}.",
|
"PURGE: {} definition(s) removed — {}.",
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,23 @@ fn cmd_input_id() -> iced::widget::Id {
|
||||||
iced::widget::Id::new(CMD_INPUT_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;
|
const MAX_HISTORY: usize = 64;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
|
|
@ -346,9 +363,12 @@ impl CommandLine {
|
||||||
// The current step's prompt is the single pinned line. When the
|
// The current step's prompt is the single pinned line. When the
|
||||||
// step offers options, render them as clickable buttons inline
|
// step offers options, render them as clickable buttons inline
|
||||||
// beside the prompt text — so options need never be typed and
|
// 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() {
|
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)
|
.spacing(6)
|
||||||
.align_y(iced::Center);
|
.align_y(iced::Center);
|
||||||
for opt in &self.step_options {
|
for opt in &self.step_options {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue