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
+61
-64
@@ -2337,80 +2337,77 @@ impl<'a> HostSession<'a> {
|
||||
Ok(handle)
|
||||
}
|
||||
TableOperation::BlockRename { from, to } => {
|
||||
let (from, handle) = editable_block(self.document(), from.trim())?;
|
||||
let to = validated_symbol_name(&to, "a block name")?;
|
||||
if to.starts_with('*') {
|
||||
return Err("a block name cannot start with '*'".to_owned());
|
||||
let from = from.trim().to_owned();
|
||||
let to = to.trim().to_owned();
|
||||
let open_edits = self.open_block_edits();
|
||||
let handle = self
|
||||
.document()
|
||||
.block_records
|
||||
.get(&from)
|
||||
.map(|record| record.handle)
|
||||
.unwrap_or(Handle::NULL);
|
||||
self.push_undo(&format!("Rename block {from} to {to}"));
|
||||
match crate::scene::block_service::rename(
|
||||
&mut self.app.tabs[self.tab].scene,
|
||||
&from,
|
||||
&to,
|
||||
&open_edits,
|
||||
) {
|
||||
Ok(_) => {
|
||||
self.finish_block_change();
|
||||
Ok(handle)
|
||||
}
|
||||
Err(error) => {
|
||||
self.app.discard_last_undo_entry(self.tab);
|
||||
Err(error.to_string())
|
||||
}
|
||||
}
|
||||
if from.eq_ignore_ascii_case(&to) {
|
||||
return Err("the new block name matches the current one (a case-only change is not supported)".to_owned());
|
||||
}
|
||||
if self.document().block_records.get(&to).is_some() {
|
||||
return Err(format!("block {to:?} already exists"));
|
||||
}
|
||||
self.push_undo("Rename block");
|
||||
if !self.app.tabs[self.tab].scene.rename_block(&from, &to) {
|
||||
return Err("the block could not be renamed".to_owned());
|
||||
}
|
||||
self.finish_block_change();
|
||||
Ok(handle)
|
||||
}
|
||||
TableOperation::BlockDelete { name } => {
|
||||
let (name, handle) = editable_block(self.document(), name.trim())?;
|
||||
let doc = self.document();
|
||||
let referenced = doc.entities().any(|entity| match entity {
|
||||
E::Insert(insert) => insert.block_name.eq_ignore_ascii_case(&name),
|
||||
E::MultiLeader(leader) => {
|
||||
leader.block_content_handle == Some(handle)
|
||||
|| leader.context.block_content_handle == Some(handle)
|
||||
let name = name.trim().to_owned();
|
||||
// The external API has no force flag, so a referenced block is
|
||||
// always refused here — same rule as the palette.
|
||||
let open_edits = self.open_block_edits();
|
||||
let handle = self
|
||||
.document()
|
||||
.block_records
|
||||
.get(&name)
|
||||
.map(|record| record.handle)
|
||||
.unwrap_or(Handle::NULL);
|
||||
self.push_undo(&format!("Delete block {name}"));
|
||||
match crate::scene::block_service::delete(
|
||||
&mut self.app.tabs[self.tab].scene,
|
||||
&name,
|
||||
crate::scene::block_service::DeleteOptions {
|
||||
force: false,
|
||||
open_edits: &open_edits,
|
||||
},
|
||||
) {
|
||||
Ok(_) => {
|
||||
self.finish_block_change();
|
||||
Ok(handle)
|
||||
}
|
||||
Err(error) => {
|
||||
self.app.discard_last_undo_entry(self.tab);
|
||||
Err(error.to_string())
|
||||
}
|
||||
_ => false,
|
||||
}) || doc.dim_styles.iter().any(|style| {
|
||||
[style.dimblk, style.dimblk1, style.dimblk2, style.dimldrblk].contains(&handle)
|
||||
});
|
||||
if referenced {
|
||||
return Err(format!(
|
||||
"block {name:?} is still inserted or used by a style or leader"
|
||||
));
|
||||
}
|
||||
let members: Vec<Handle> = {
|
||||
let record = doc.block_records.get(&name).expect("checked above");
|
||||
let mut members = record.entity_handles.clone();
|
||||
members.push(record.block_entity_handle);
|
||||
members.push(record.block_end_handle);
|
||||
members
|
||||
};
|
||||
self.push_undo("Delete block");
|
||||
let doc = self.document_mut();
|
||||
for member in members {
|
||||
doc.remove_entity(member);
|
||||
}
|
||||
doc.block_records.remove(&name);
|
||||
let orphans: Vec<Handle> = {
|
||||
let live: std::collections::HashSet<Handle> =
|
||||
doc.block_records.iter().map(|r| r.handle).collect();
|
||||
doc.objects
|
||||
.iter()
|
||||
.filter_map(|(h, object)| match object {
|
||||
codec::objects::ObjectType::SortEntitiesTable(table)
|
||||
if !live.contains(&table.block_owner_handle) =>
|
||||
{
|
||||
Some(*h)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
for orphan in orphans {
|
||||
doc.objects.remove(&orphan);
|
||||
}
|
||||
self.finish_block_change();
|
||||
Ok(handle)
|
||||
}
|
||||
_ => Err("unsupported table operation".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Block records this document has open in a BEDIT session. Nested
|
||||
/// sessions all count, so the whole stack is handed to the block service,
|
||||
/// which refuses delete/rename and skips PURGE for every one of them.
|
||||
fn open_block_edits(&self) -> Vec<Handle> {
|
||||
self.app.tabs[self.tab]
|
||||
.block_edits
|
||||
.iter()
|
||||
.map(|session| session.br_handle)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn finish_block_change(&mut self) {
|
||||
self.app.tabs[self.tab].dirty = true;
|
||||
self.app.tabs[self.tab].scene.invalidate_dependency_index();
|
||||
|
||||
Reference in new issue
Block a user