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:
Karim Jerbi committed 2026-10-12 07:25:11 +01:00
1 parent 5dfa41c032
commit 2b50b6c858
16 files changed
+2701 -262

No files matched your search

+2 -2
View File
@@ -316,10 +316,10 @@ registry with write rules), `properties`, `measure` (length/area/bounds),
| Op | Fields | Semantics |
|---|---|---|
| `block_define` | `name`, `base:[x,y,z]`, `handles`, optional `insert_at`, optional `"replace":true` | The sources move into the new definition (flattened at `base` = block origin) and one `Insert` is placed at `insert_at` (default `base`), so the drawing looks unchanged. `"replace":true` drops a same-named definition — children, markers and inserts — inside the same undo step before creating the new one (idempotent re-import). Result `{"block":name,"insert":"<handle>"}`. Names are unique, `*` is reserved. |
| `block_delete` | `name` | Removes the definition completely: child entities, block markers, every `Insert` referencing it and the table record. One undo step. Result `erased`. Unknown → `block_missing`; `*`-records → `block_protected`. |
| `block_delete` | `name`, optional `"force":true` | Removes the definition completely: child entities, block markers and the table record. One undo step (`Delete block <name>`). A referenced block is refused (`block_referenced`); `"force":true` erases the referencing INSERTs in the same step and reports `result.references`. Result `erased`. Unknown → `block_missing`; layout/anonymous/xref/dynamic records → `block_protected`; open BEDIT session → `block_in_edit`; invalid/taken names → `block_invalid_name` / `block_exists`. |
| `wblock` | `path`, `handles` *or* `block`; optional `"template":"<file>"` (the export inherits that file's tables/styles), optional `"normalize":true` (the export is shifted so its overall bounds minimum lands on the origin) | Writes a standalone DWG/DXF (extension decides). The source document is untouched. Result `{path, entities, normalized}`. |
REST: `POST /blocks`, `DELETE /blocks/{name}`, `POST /wblock`.
REST: `POST /blocks`, `DELETE /blocks/{name}` (add `?force=true` to erase the referencing INSERTs instead of being refused), `POST /wblock`.
### 6.6 Layouts and plotting
+9 -3
View File
@@ -701,9 +701,15 @@ definition — its children, markers and inserts — inside the same undo step
before creating the new one: that is SPM's barcode re-import flow, so
`block_define` never fails with "already exists" on a re-import.
`block_delete` removes a definition completely (children, Block/BlockEnd
markers, every Insert referencing it and the table record) and reports
`result.erased`; unknown names are `block_missing`, `*`-prefixed layout
records are `block_protected`. Definitions survive save/open round trips
markers and the table record). A definition anything still points at — an
INSERT, a dimension-style arrow block, a multi-leader's block content — is
refused with `block_referenced` and the shared message text; add
`"force":true` to erase the referencing INSERTs in the same undo step and get
`result.references` with the number that were overridden. `result.erased`
counts every entity the step removed. Unknown names are `block_missing`,
`*`-prefixed layout records, anonymous and xref definitions are
`block_protected`, and a block with an open BEDIT session is `block_in_edit`.
Definitions survive save/open round trips
and are consumable by `wblock` (by block name) and `INSERT` creation via
`entities_create`.
+54 -107
View File
@@ -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.",
+42 -23
View File
@@ -899,33 +899,37 @@ impl OpenCADStudio {
Ok(Task::none())
}
/// `block_delete` — remove a block definition together with its child
/// entities, the Block/BlockEnd markers and every Insert referencing it.
/// `block_delete` — remove a block definition. A definition something
/// still points at is refused unless the request carries `"force": true`,
/// which erases the referencing INSERTs in the same undo step.
pub(super) fn control_block_delete(&mut self, req: &Value) -> Result<Task<Message>, Value> {
let name = string(req, "name")?.to_owned();
if name.starts_with('*') {
return Err(failure(
"block_protected",
"Layout block records cannot be deleted",
));
}
let force = req["force"].as_bool().unwrap_or(false);
let i = self.active_tab;
if self.tabs[i]
.scene
.document
.block_records
.get(&name)
.is_none()
{
return Err(failure(
"block_missing",
format!("Block '{name}' does not exist"),
));
}
self.push_undo_snapshot(i, "BLOCKDELETE");
let erased = erase_block_definition(&mut self.tabs[i].scene, &name);
let open_edits: Vec<codec::Handle> = self.tabs[i]
.block_edits
.iter()
.map(|session| session.br_handle)
.collect();
self.push_undo_snapshot(i, format!("Delete block {name}"));
let options = crate::scene::block_service::DeleteOptions {
force,
open_edits: &open_edits,
};
let outcome =
match crate::scene::block_service::delete(&mut self.tabs[i].scene, &name, options) {
Ok(outcome) => outcome,
Err(error) => {
self.discard_last_undo_entry(i);
return Err(failure(block_error_code(&error), error.to_string()));
}
};
self.post_ref_op(i);
self.set_control_result(json!({ "block": name, "erased": erased }));
self.set_control_result(json!({
"block": outcome.name,
"erased": outcome.erased,
"references": outcome.references.len(),
}));
Ok(Task::none())
}
@@ -988,6 +992,21 @@ pub(crate) fn erase_block_definition(scene: &mut crate::scene::Scene, name: &str
victims.len()
}
/// The automation error code for each [`BlockServiceError`], so a headless
/// client branches on a stable machine-readable code while the text stays the
/// one the shared formatter produced.
fn block_error_code(error: &crate::scene::block_service::BlockServiceError) -> &'static str {
use crate::scene::block_service::BlockServiceError as E;
match error {
E::NotFound(_) => "block_missing",
E::InvalidName(_) => "block_invalid_name",
E::NameExists(_) => "block_exists",
E::Referenced { .. } => "block_referenced",
E::InEditSession(_) => "block_in_edit",
E::Protected(_) => "block_protected",
}
}
/// `data:[{code,value},…]` → typed XData values; an absent or empty list
/// means "remove this application's record".
fn parse_xdata_values(req: &Value) -> Parsed<Option<Vec<codec::xdata::XDataValue>>> {
-1
View File
@@ -1631,7 +1631,6 @@ mod actions;
pub(crate) mod vision;
pub(crate) use actions::property_json;
mod entities;
pub(crate) use entities::erase_block_definition;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod http_bridge;
mod interactive;
+19 -1
View File
@@ -551,11 +551,29 @@ fn block_delete_and_define_replace_manage_definitions() {
assert_eq!(count_type(&mut app, "Line"), 0, "old children erased");
assert_eq!(count_type(&mut app, "Circle"), 0, "old children erased");
// A definition the insert still points at is refused; "force":true erases
// the referencing inserts in the same undo step.
let r = app.automation_op(&format!(
r#"{{"protocol":1,"op":"block_delete","request_id":"bl1b","document_id":{doc},"name":"MARK"}}"#
));
assert_eq!(r["ok"], false);
assert_eq!(r["code"], "block_referenced");
assert_eq!(
count_type(&mut app, "Insert"),
1,
"the insert survived the refusal"
);
// Delete removes the insert, the markers, the children and the record.
let r = app.automation_op(&format!(
r#"{{"protocol":1,"op":"block_delete","request_id":"bl2","document_id":{doc},"name":"MARK"}}"#
r#"{{"protocol":1,"op":"block_delete","request_id":"bl2","document_id":{doc},"name":"MARK","force":true}}"#
));
assert_eq!(r["ok"], true, "{}", r["error"]);
assert_eq!(
r["result"]["references"].as_u64().unwrap(),
1,
"the forced delete reports what it overrode"
);
assert!(
r["result"]["erased"].as_u64().unwrap() >= 3,
"insert + markers + child"
+61 -64
View File
@@ -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();
File diff suppressed because it is too large. Load diff
+25 -14
View File
@@ -491,20 +491,31 @@ impl OpenCADStudio {
Tab::Current => {
let name = item.name();
let i = self.active_tab;
let referenced = self.tabs[i].scene.document.entities().any(|e| {
matches!(e, codec::EntityType::Insert(ins) if ins.block_name.eq_ignore_ascii_case(&name))
});
if referenced {
self.command_line.push_error(&crate::tf!(
"Block \"{name}\" is referenced in the drawing and cannot be removed."
));
} else {
self.push_undo_snapshot(i, "PURGE");
crate::app::control::erase_block_definition(&mut self.tabs[i].scene, &name);
self.tabs[i].dirty = true;
self.command_line
.push_output(&crate::tf!("Block \"{name}\" removed."));
self.refresh_block_palette();
let open_edits: Vec<codec::Handle> = self.tabs[i]
.block_edits
.iter()
.map(|session| session.br_handle)
.collect();
self.push_undo_snapshot(i, format!("Delete block {name}"));
let outcome = crate::scene::block_service::delete(
&mut self.tabs[i].scene,
&name,
crate::scene::block_service::DeleteOptions {
force: false,
open_edits: &open_edits,
},
);
match outcome {
Ok(_) => {
self.tabs[i].dirty = true;
self.command_line
.push_output(&crate::tf!("Block \"{name}\" removed."));
self.refresh_block_palette();
}
Err(error) => {
self.discard_last_undo_entry(i);
self.command_line.push_error(&error.to_string());
}
}
}
Tab::Recent | Tab::Favorites => {
+2
View File
@@ -93,6 +93,8 @@ fn reorder_insertion_index(from: usize, to: usize, after: bool, len: usize) -> O
(insertion != from).then_some(insertion)
}
#[cfg(test)]
mod block_ops_tests;
mod blocks_palette;
mod command;
mod context_menu;
+3 -1
View File
@@ -28,8 +28,10 @@ const MODERN_PROTOCOL_VERSION: &str = "2026-07-28";
/// The `tool_schema_digest_is_pinned` test fails whenever the MCP tool surface
/// changes, so schema drift (new params, renamed tools) is always a conscious,
/// reviewed edit — and clients can detect a stale bridge by comparing digests.
///
/// `block_delete` gained its optional `force` parameter.
#[cfg(test)]
const TOOL_SCHEMA_DIGEST: &str = "e7bd01696f57143d7139aeb75d53389450b1102b603f2ba4ace8122cc692070e";
const TOOL_SCHEMA_DIGEST: &str = "c16572f139198d90807ede156370879106672ed97566c16ab6ae001c996059e8";
const MAX_REQUEST: usize = 1_048_576;
const MAX_RESPONSE: u64 = 16 * 1024 * 1024;
const CACHE_TTL_MS: u64 = 3_600_000;
+10 -3
View File
@@ -534,11 +534,18 @@ pub static BLOCK_DEFINE: OpDef = OpDef {
pub static BLOCK_DELETE: OpDef = OpDef {
name: "block_delete",
doc: "Delete a block definition and all its child entities and inserts.",
doc: "Delete a block definition and all its child entities. A referenced block is refused; force erases the referencing inserts.",
batchable: true,
params: &[req("name", Ty::Str, "Block name to delete.")],
params: &[
req("name", Ty::Str, "Block name to delete."),
opt(
"force",
Ty::Bool,
"Erase the referencing inserts instead of refusing a referenced block. Result reports how many references were overridden.",
),
],
rules: &[],
example: r#"{"op":"block_delete","name":"DOOR"}"#,
example: r#"{"op":"block_delete","name":"DOOR","force":true}"#,
};
pub static XDATA_SET: OpDef = OpDef {
+24 -5
View File
@@ -676,11 +676,22 @@ pub(crate) fn plan(method: &str, rest: &[&str], request: &HttpRequest) -> Plan {
fields: body,
created: 201,
},
("DELETE", ["blocks", name]) => Plan::Mutate {
op: "block_delete".into(),
fields: json!({"name": name}),
created: 200,
},
("DELETE", ["blocks", name]) => {
// `?force=true` mirrors the control op: it erases the referencing
// inserts instead of refusing a referenced definition.
let mut fields = json!({"name": name});
if request
.param("force")
.is_some_and(|value| value == "true" || value == "1")
{
fields["force"] = json!(true);
}
Plan::Mutate {
op: "block_delete".into(),
fields,
created: 200,
}
}
("POST", ["file-identity"]) => Plan::Mutate {
op: "file_identity".into(),
fields: body,
@@ -1440,13 +1451,21 @@ mod tests {
&mut counter,
);
assert_eq!(status, 201, "{body}");
// A definition the insert still points at is refused without force.
let (status, body) = route(
&mut app,
&request("DELETE", "/api/v1/blocks/MARK", ""),
&mut document_id,
&mut counter,
);
assert_eq!(status, 400, "{body}");
assert_eq!(body["code"], "block_referenced", "{body}");
// `?force=true` erases the referencing insert in the same step.
let mut forced = request("DELETE", "/api/v1/blocks/MARK", "");
forced.query = vec![("force".into(), "true".into())];
let (status, body) = route(&mut app, &forced, &mut document_id, &mut counter);
assert_eq!(status, 200, "{body}");
assert_eq!(body["result"]["references"], 1, "{body}");
assert!(body["result"]["erased"].as_u64().unwrap() >= 1);
let (status, body) = route(
&mut app,
File diff suppressed because it is too large. Load diff
+1
View File
@@ -6,6 +6,7 @@
// view — camera, transforms, viewport, render pipeline driver
// cache — block-definition and property caches
pub mod annotative;
pub mod block_service;
pub mod cache;
pub mod convert;
pub mod creation_style;
-38
View File
@@ -1027,44 +1027,6 @@ pub fn entity_block_uses(
uses
}
/// Blocks referenced by document-level styles rather than a concrete entity.
pub fn document_block_uses(document: &CadDocument) -> Vec<BlockUse> {
let mut uses = Vec::new();
for style in document.dim_styles.iter() {
for handle in [style.dimblk, style.dimblk1, style.dimblk2, style.dimldrblk] {
push_dependency_use(&mut uses, document, handle, BlockRole::ArrowHead);
}
for name in [&style.dimblk_name, &style.dimblk1_name, &style.dimblk2_name] {
if let Some(record) = block_record_by_name(document, name) {
push_dependency_use(&mut uses, document, record.handle, BlockRole::ArrowHead);
}
}
}
for object in document.objects.values() {
let codec::objects::ObjectType::MultiLeaderStyle(style) = object else {
continue;
};
if let Some(handle) = style.arrowhead_handle {
push_dependency_use(&mut uses, document, handle, BlockRole::ArrowHead);
}
if let Some(handle) = style.block_content_handle {
push_dependency_use(&mut uses, document, handle, BlockRole::MultiLeaderContent);
}
}
for line_type in document.line_types.iter() {
for symbol in crate::scene::convert::dgn_linestyle::symbol_blocks(document, &line_type.name)
{
push_dependency_use(
&mut uses,
document,
symbol.block,
BlockRole::LineStyleSymbol,
);
}
}
uses
}
const MAX_MINSERT_INSTANCES: usize = 20_000;
pub fn array_offsets(insert: &Insert) -> Vec<[f64; 3]> {