feat: XREF management — auto-resolve, XATTACH, XREF, XRELOAD
- src/io/xref.rs: resolve_xrefs() scans block_records for is_xref flags,
resolves paths relative to the opened file's directory, loads the
referenced DWG/DXF and merges model-space entities into the xref block
- src/modules/insert/xattach.rs: XATTACH command with file-picker flow
(XAttachPick/XAttachPickResult messages), prepare_xref_block() creates
BlockRecord + Block entities and immediately resolves content
- XREF command: lists all external references in the drawing
- XRELOAD command: re-resolves all xref paths and rebuilds scene caches
- Auto-resolve on FileOpened: reports Loaded/NotFound per xref block
- CadCommand trait: added optional xattach_path() method
- ROADMAP.md: 1.9 XREF management marked ✅
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4c1c13cab6
commit
64837e9aed
10 changed files with 487 additions and 3 deletions
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
pub mod pdf_export;
|
||||
pub mod plot_style;
|
||||
pub mod xref;
|
||||
|
||||
use acadrust::io::dwg::DwgReader;
|
||||
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
||||
|
|
|
|||
143
src/io/xref.rs
Normal file
143
src/io/xref.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// XREF resolution — scan a loaded document for external-reference blocks and
|
||||
// populate them with geometry from the referenced DWG/DXF files.
|
||||
|
||||
use acadrust::entities::{Block, BlockEnd};
|
||||
use acadrust::types::{Handle, Vector3};
|
||||
use acadrust::{CadDocument, EntityType};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Status of an external reference block.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum XrefStatus {
|
||||
/// File was found and loaded successfully.
|
||||
Loaded,
|
||||
/// File path is set but the file could not be found or read.
|
||||
NotFound,
|
||||
}
|
||||
|
||||
/// Describes a single external reference found in a document.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct XrefInfo {
|
||||
/// Block name (e.g. the filename stem).
|
||||
pub name: String,
|
||||
/// Resolved file path (or raw path if not found).
|
||||
pub path: String,
|
||||
pub status: XrefStatus,
|
||||
}
|
||||
|
||||
/// Scan `doc` for XREF block-records, resolve their paths relative to
|
||||
/// `base_dir`, and populate each xref block with entities from the
|
||||
/// referenced file.
|
||||
///
|
||||
/// Returns a list of [`XrefInfo`] describing each xref block found.
|
||||
pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> Vec<XrefInfo> {
|
||||
// Collect xref blocks: (name, raw_path, block_record_handle)
|
||||
let xref_entries: Vec<(String, String, Handle)> = doc
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|br| (br.flags.is_xref || br.flags.is_xref_overlay) && !br.xref_path.is_empty())
|
||||
.map(|br| (br.name.clone(), br.xref_path.clone(), br.handle))
|
||||
.collect();
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
for (block_name, raw_path, br_handle) in xref_entries {
|
||||
let resolved = resolve_path(&raw_path, base_dir);
|
||||
|
||||
let status = match &resolved {
|
||||
None => XrefStatus::NotFound,
|
||||
Some(p) => match super::load_file(p) {
|
||||
Err(_) => XrefStatus::NotFound,
|
||||
Ok(xref_doc) => {
|
||||
ensure_block_entities(doc, &block_name);
|
||||
merge_xref_into_block(doc, br_handle, xref_doc);
|
||||
XrefStatus::Loaded
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
result.push(XrefInfo {
|
||||
name: block_name,
|
||||
path: resolved
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or(raw_path),
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Try to build an absolute path from a raw xref path string.
|
||||
/// Handles absolute paths, relative paths, and Windows-style separators.
|
||||
fn resolve_path(raw: &str, base_dir: &Path) -> Option<PathBuf> {
|
||||
let normalised = raw.replace('\\', "/");
|
||||
let p = PathBuf::from(&normalised);
|
||||
|
||||
if p.is_absolute() {
|
||||
if p.exists() {
|
||||
return Some(p);
|
||||
}
|
||||
// Fallback: try the filename in base_dir.
|
||||
if let Some(fname) = p.file_name() {
|
||||
let c = base_dir.join(fname);
|
||||
if c.exists() {
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Relative path against base_dir.
|
||||
let candidate = base_dir.join(&p);
|
||||
if candidate.exists() {
|
||||
return Some(candidate);
|
||||
}
|
||||
|
||||
// Last resort: just the filename.
|
||||
if let Some(fname) = p.file_name() {
|
||||
let c = base_dir.join(fname);
|
||||
if c.exists() {
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Make sure `doc` has BLOCK + ENDBLK entities for `block_name`.
|
||||
/// These are required so renderers can find the block content.
|
||||
fn ensure_block_entities(doc: &mut CadDocument, block_name: &str) {
|
||||
let has_block = doc.entities().any(|e| {
|
||||
matches!(e, EntityType::Block(b) if b.name == block_name)
|
||||
});
|
||||
if has_block {
|
||||
return;
|
||||
}
|
||||
let b = Block::new(block_name, Vector3::zero());
|
||||
let _ = doc.add_entity(EntityType::Block(b));
|
||||
let _ = doc.add_entity(EntityType::BlockEnd(BlockEnd::new()));
|
||||
}
|
||||
|
||||
/// Copy model-space entities from `xref_doc` into the xref block (`br_handle`)
|
||||
/// of `doc`.
|
||||
fn merge_xref_into_block(doc: &mut CadDocument, br_handle: Handle, xref_doc: CadDocument) {
|
||||
let entities: Vec<EntityType> = xref_doc
|
||||
.entities()
|
||||
.filter(|e| !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for mut entity in entities {
|
||||
// Clear the foreign handle so acadrust assigns a new one.
|
||||
set_handle(&mut entity, Handle::NULL);
|
||||
// Route to the xref block record.
|
||||
entity.common_mut().owner_handle = br_handle;
|
||||
let _ = doc.add_entity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the handle field of any entity variant.
|
||||
fn set_handle(entity: &mut EntityType, h: Handle) {
|
||||
entity.common_mut().handle = h;
|
||||
}
|
||||
Loading…
Reference in a new issue