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:
Hakan Seven 2026-04-08 00:02:38 +03:00
commit 64837e9aed
10 changed files with 487 additions and 3 deletions

View file

@ -65,6 +65,27 @@ impl H7CAD {
self.refresh_properties();
}
CmdResult::CommitAndExit(entity) => {
// For XATTACH: ensure the xref block definition exists before
// committing the INSERT entity that references it.
// Extract path early to avoid borrow conflicts.
let xattach_path: Option<String> = {
let tab = &self.tabs[i];
if let Some(cmd) = tab.active_cmd.as_ref() {
if cmd.name() == "XATTACH" {
cmd.xattach_path()
} else {
None
}
} else {
None
}
};
if let Some(path) = xattach_path {
crate::modules::insert::xattach::prepare_xref_block(
&mut self.tabs[i].scene,
&path,
);
}
let label = self.history_label_from_active_cmd(i, "ENTITY");
self.push_undo_snapshot(i, label);
self.commit_entity(entity);

View file

@ -417,6 +417,74 @@ impl H7CAD {
}
}
"XATTACH" | "XA" => {
// Launch the file picker; XAttachPickResult will start the command.
return Task::done(Message::XAttachPick);
}
"XREF" | "XR" => {
// List all xref blocks in the current drawing.
let xrefs: Vec<String> = self.tabs[i]
.scene
.document
.block_records
.iter()
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.map(|br| {
format!(
" {} — {}",
br.name,
if br.xref_path.is_empty() {
"(no path)".to_string()
} else {
br.xref_path.clone()
}
)
})
.collect();
if xrefs.is_empty() {
self.command_line.push_output("XREF No external references in this drawing.");
} else {
self.command_line.push_output("XREF External references:");
for line in xrefs {
self.command_line.push_output(&line);
}
}
}
"XRELOAD" => {
// Reload all xrefs for the current drawing.
if let Some(path) = &self.tabs[i].current_path.clone() {
if let Some(base_dir) = path.parent() {
let infos = crate::io::xref::resolve_xrefs(
&mut self.tabs[i].scene.document,
base_dir,
);
for info in &infos {
match info.status {
crate::io::xref::XrefStatus::Loaded => {
self.command_line.push_output(&format!(
"XREF Reloaded \"{}\"",
info.name
));
}
crate::io::xref::XrefStatus::NotFound => {
self.command_line.push_error(&format!(
"XREF Not found: \"{}\" ({})",
info.name, info.path
));
}
}
}
self.tabs[i].scene.populate_hatches_from_document();
self.tabs[i].scene.populate_images_from_document();
self.tabs[i].scene.populate_meshes_from_document();
}
} else {
self.command_line.push_error("XREF Save the drawing first to resolve relative XREF paths.");
}
}
// ── Draw commands ──────────────────────────────────────────────
"LINE"|"L" => {
use crate::modules::home::draw::line::LineCommand;

View file

@ -412,6 +412,11 @@ pub enum Message {
ImagePick,
/// Result of the image file picker + pixel dimension decode.
ImagePickResult(Result<(std::path::PathBuf, u32, u32), String>),
// ── XREF ──────────────────────────────────────────────────────────────
/// Open file-picker dialog for XATTACH command (async).
XAttachPick,
/// Result of the XATTACH file picker.
XAttachPickResult(Result<std::path::PathBuf, String>),
}
impl H7CAD {

View file

@ -46,8 +46,33 @@ impl H7CAD {
idx
};
self.tabs[i].current_path = Some(path);
self.tabs[i].current_path = Some(path.clone());
self.tabs[i].scene.document = doc;
// Auto-resolve XREFs relative to the opened file's directory.
if let Some(base_dir) = path.parent() {
let xrefs = crate::io::xref::resolve_xrefs(
&mut self.tabs[i].scene.document,
base_dir,
);
for info in &xrefs {
match info.status {
crate::io::xref::XrefStatus::Loaded => {
self.command_line.push_output(&format!(
"XREF Loaded \"{}\"",
info.name
));
}
crate::io::xref::XrefStatus::NotFound => {
self.command_line.push_error(&format!(
"XREF Not found: \"{}\" ({})",
info.name, info.path
));
}
}
}
}
self.tabs[i].scene.populate_hatches_from_document();
self.tabs[i].scene.populate_images_from_document();
self.tabs[i].scene.populate_meshes_from_document();
@ -103,6 +128,41 @@ impl H7CAD {
Task::none()
}
Message::XAttachPick => Task::perform(
async {
let handle = rfd::AsyncFileDialog::new()
.set_title("Select External Reference File")
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
.add_filter("DWG Files", &["dwg", "DWG"])
.add_filter("DXF Files", &["dxf", "DXF"])
.pick_file()
.await;
match handle {
Some(h) => Ok(h.path().to_path_buf()),
None => Err("Cancelled".to_string()),
}
},
Message::XAttachPickResult,
),
Message::XAttachPickResult(Ok(path)) => {
use crate::command::CadCommand;
use crate::modules::insert::xattach::XAttachCommand;
let path_str = path.to_string_lossy().into_owned();
let cmd = XAttachCommand::with_path(path_str);
let i = self.active_tab;
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
Task::none()
}
Message::XAttachPickResult(Err(e)) => {
if e != "Cancelled" {
self.command_line.push_error(&format!("XATTACH: {e}"));
}
Task::none()
}
Message::SaveFile => {
let i = self.active_tab;
if let Some(path) = &self.tabs[i].current_path {