forked from 1b-agent/opencadstudio-upstream
fix(blocks): one block definition service for delete, rename and purge on every path
new src/scene/block_service.rs (references, delete, rename, purge candidates, name validation, one error type and message). The command-line RENAME, palette Remove, plugin API, control/MCP block_delete and PURGE all use it. Layout, xref, dynamic-block, anonymous and open-in-editor records are protected; PURGE removes an anonymous block only when nothing references it. PROTOCOL CHANGE: control/MCP block_delete refuses a referenced block unless force=true (erases referencing INSERTs in one undo step). New REST route DELETE /api/v1/blocks/{name}?force=true. MCP tool digest re-pinned. Undo labels are Delete block X, Rename block X to Y and Purge.
This commit is contained in:
1 parent
5dfa41c032
commit
2b50b6c858
16 files changed
+2701
-262
No files matched your search
+54
-107
@@ -358,62 +358,6 @@ impl OpenCADStudio {
|
||||
.map(|s| s.to_uppercase())
|
||||
.collect();
|
||||
|
||||
// Live blocks are reachable from layouts and style roots
|
||||
// through the shared graph. Dangling layouts do not count.
|
||||
let live_blocks: rustc_hash::FxHashSet<String> = {
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let is_real_layout = |br: &codec::BlockRecord| -> bool {
|
||||
let up = br.name.to_ascii_uppercase();
|
||||
up.starts_with("*MODEL_SPACE")
|
||||
|| up.starts_with("*PAPER_SPACE")
|
||||
|| matches!(
|
||||
doc.objects.get(&br.layout),
|
||||
Some(codec::objects::ObjectType::Layout(_))
|
||||
)
|
||||
};
|
||||
let children = |name: &str| -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
let Some(br) = doc.block_records.get(name) else {
|
||||
return out;
|
||||
};
|
||||
for &h in &br.entity_handles {
|
||||
let Some(entity) = doc.get_entity(h) else {
|
||||
continue;
|
||||
};
|
||||
out.extend(
|
||||
crate::scene::render_graph::entity_block_uses(doc, entity, 1.0)
|
||||
.into_iter()
|
||||
.filter(|block_use| !block_use.block.is_null())
|
||||
.map(|block_use| block_use.insert.block_name),
|
||||
);
|
||||
}
|
||||
out
|
||||
};
|
||||
let mut live = rustc_hash::FxHashSet::default();
|
||||
let mut stack: Vec<String> = doc
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|&br| is_real_layout(br))
|
||||
.map(|br| br.name.clone())
|
||||
.collect();
|
||||
stack.extend(
|
||||
crate::scene::render_graph::document_block_uses(doc)
|
||||
.into_iter()
|
||||
.map(|block_use| block_use.insert.block_name),
|
||||
);
|
||||
while let Some(n) = stack.pop() {
|
||||
if !live.insert(n.clone()) {
|
||||
continue;
|
||||
}
|
||||
for c in children(&n) {
|
||||
if !live.contains(&c) {
|
||||
stack.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
live
|
||||
};
|
||||
|
||||
// Build removal lists (still immutable)
|
||||
let layer_remove: Vec<String> = if do_layers {
|
||||
self.tabs[i]
|
||||
@@ -466,26 +410,21 @@ impl OpenCADStudio {
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Remove unreachable blocks but retain live xrefs and space containers.
|
||||
// Remove blocks nothing references any more. The shared
|
||||
// service decides what qualifies — layout/xref/dynamic
|
||||
// records and open BEDIT sessions are protected, and so is
|
||||
// every reference kind — so a purge and a delete give the
|
||||
// same answer.
|
||||
let block_remove: Vec<String> = if do_blocks {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
let open_edits: Vec<codec::Handle> = self.tabs[i]
|
||||
.block_edits
|
||||
.iter()
|
||||
.filter(|br| {
|
||||
let up = br.name.to_ascii_uppercase();
|
||||
// Never PURGE an xref *definition* — that is a
|
||||
// detach, not a purge. Orphaned dependencies from
|
||||
// a past detach still use the reachability test.
|
||||
!br.flags.is_xref
|
||||
&& !br.flags.is_xref_overlay
|
||||
&& !up.starts_with("*MODEL_SPACE")
|
||||
&& !up.starts_with("*PAPER_SPACE")
|
||||
&& !live_blocks.contains(&br.name)
|
||||
})
|
||||
.map(|br| br.name.clone())
|
||||
.collect()
|
||||
.map(|session| session.br_handle)
|
||||
.collect();
|
||||
crate::scene::block_service::purge_candidates(
|
||||
&self.tabs[i].scene.document,
|
||||
&open_edits,
|
||||
)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
@@ -501,7 +440,7 @@ impl OpenCADStudio {
|
||||
// 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");
|
||||
self.push_undo_snapshot(i, "Purge");
|
||||
snapshot_pushed = true;
|
||||
}
|
||||
|
||||
@@ -523,26 +462,11 @@ impl OpenCADStudio {
|
||||
.count();
|
||||
let mut blocks_gone = 0usize;
|
||||
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);
|
||||
h
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
for h in handles {
|
||||
self.tabs[i].scene.document.remove_entity(h);
|
||||
}
|
||||
if self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.remove(name)
|
||||
// The service erases the definition, its member
|
||||
// entities, both BLOCK/ENDBLK delimiters and any stale
|
||||
// draw-order table, so a purge and a delete leave the
|
||||
// same document behind.
|
||||
if crate::scene::block_service::erase(&mut self.tabs[i].scene, name)
|
||||
.is_some()
|
||||
{
|
||||
blocks_gone += 1;
|
||||
@@ -586,7 +510,7 @@ impl OpenCADStudio {
|
||||
// re-assignment needed (a block purge already snapshotted;
|
||||
// a snapshot here only matters when nothing else changed).
|
||||
if !snapshot_pushed {
|
||||
self.push_undo_snapshot(i, "PURGE");
|
||||
self.push_undo_snapshot(i, "Purge");
|
||||
}
|
||||
for h in &orphans {
|
||||
self.tabs[i].scene.document.objects.remove(h);
|
||||
@@ -3392,7 +3316,13 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
// Snapshot BEFORE mutating (undo must restore the old
|
||||
// name); dropped again if nothing was renamed.
|
||||
self.push_undo_snapshot(i, "RENAME");
|
||||
let block = type_str == "BLOCK";
|
||||
let label = if block {
|
||||
format!("Rename block {old_name} to {new_name}")
|
||||
} else {
|
||||
"RENAME".to_string()
|
||||
};
|
||||
self.push_undo_snapshot(i, label);
|
||||
let known = matches!(
|
||||
type_str.as_str(),
|
||||
"LAYER"
|
||||
@@ -3405,11 +3335,31 @@ impl OpenCADStudio {
|
||||
| "UCS"
|
||||
| "VIEW"
|
||||
);
|
||||
let ok = if type_str == "BLOCK" {
|
||||
// The block path's refusal text comes from the shared
|
||||
// service, so the command line, the palette, the plugin
|
||||
// API and the control op all word it the same way.
|
||||
let mut block_error: Option<String> = None;
|
||||
let ok = if block {
|
||||
let open_edits: Vec<codec::Handle> = self.tabs[i]
|
||||
.block_edits
|
||||
.iter()
|
||||
.map(|session| session.br_handle)
|
||||
.collect();
|
||||
// Re-keys the record, syncs the Block marker and every
|
||||
// INSERT reference; refuses anonymous/xref sources and
|
||||
// invalid/taken names.
|
||||
self.tabs[i].scene.rename_block(&old_name, &new_name)
|
||||
// INSERT reference; refuses anonymous/xref sources,
|
||||
// protected records and invalid/taken names.
|
||||
match crate::scene::block_service::rename(
|
||||
&mut self.tabs[i].scene,
|
||||
&old_name,
|
||||
&new_name,
|
||||
&open_edits,
|
||||
) {
|
||||
Ok(_) => true,
|
||||
Err(error) => {
|
||||
block_error = Some(error.to_string());
|
||||
false
|
||||
}
|
||||
}
|
||||
} else if type_str == "LAYER" {
|
||||
self.tabs[i].rename_layer(&old_name, &new_name)
|
||||
} else if known {
|
||||
@@ -3444,13 +3394,10 @@ impl OpenCADStudio {
|
||||
);
|
||||
} else {
|
||||
self.discard_last_undo_entry(i);
|
||||
if !known {
|
||||
if let Some(message) = block_error {
|
||||
self.command_line.push_error(&message);
|
||||
} else if !known {
|
||||
self.command_line.push_error(crate::tf!("RENAME: unknown type '{}'. Use LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW", type_str).as_ref());
|
||||
} else if type_str == "BLOCK" {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"RENAME: could not rename block '{}' — missing/anonymous/xref, or '{}' is invalid or taken.",
|
||||
old_name, new_name
|
||||
).as_ref());
|
||||
} else {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"RENAME: could not rename '{}' in {} — not found or protected, or '{}' is invalid or taken.",
|
||||
|
||||
Reference in new issue
Block a user