Xref attach: Attach dialog, reference prompts, and xref drawings other applications can open

Attach External Reference dialog after XATTACH / ATTACH; ATTACH routes
drawings, PDFs and images; placement prompts and messages follow the
reference (presets, Corner/XYZ, nonzero check, existing definition reuse,
missing file, self-reference); name keeps the file's case; overlay and
path type at attach; one undo step for definition and INSERT; -XREF option
prompt and `?` table; XREF opens the palette; ER alias; xref Properties
rows; dependent symbols flagged and remapped and resolved content left out
of the saved file; dialog_ok automation action; localized labels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-23 14:44:12 +03:00
1 parent f2d534bf71
commit 6bd55bc32e
44 files changed
+2514 -359

No files matched your search

+9 -8
View File
@@ -663,10 +663,6 @@ impl OpenCADStudio {
"PDFATTACH" => {
return Some(Task::done(Message::PdfAttachPick));
}
"XATTACH" => {
// Launch the file picker; XAttachPickResult will start the command.
return Some(Task::done(Message::XAttachPick));
}
cmd if cmd == "WBLOCK" || cmd == "WB" || cmd.starts_with("WBLOCK ") => {
let arg = cmd.splitn(2, ' ').nth(1).unwrap_or("").trim();
if arg.is_empty() {
@@ -1366,11 +1362,16 @@ impl OpenCADStudio {
if after.is_empty() {
return Some(Task::done(Message::XAttachPick));
}
let cmd = crate::modules::insert::xattach::XAttachCommand::with_path(
after.to_string(),
self.start_xref_attach(
i,
crate::modules::insert::xattach::XrefAttachRequest {
path: after.trim_matches('"').to_string(),
overlay: false,
path_type: crate::io::xref_model::Pathtype::Full,
},
crate::modules::insert::xattach::XrefPlacement::on_screen(),
"-XREF",
);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
} else {
self.command_line.push_error(crate::tf!(
"XREF: unknown option '{}'. Options: ? Reload Unload Detach Path Pathtype Bind Overlay Attach",
+5
View File
@@ -5,6 +5,7 @@ use iced::Task;
use std::path::PathBuf;
mod blocks;
mod xref_attach;
mod dim;
pub(crate) mod display;
mod draw;
@@ -246,6 +247,9 @@ impl OpenCADStudio {
if let Some(t) = self.dispatch_layers(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_xref_attach(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_blocks(cmd, i) {
return Some(t);
}
@@ -652,6 +656,7 @@ inventory::submit!(crate::command::CommandRegistration {
"3DORBIT",
"3O",
"ABOUT",
"ATTACH",
"ATTDISP",
"ATTEXT",
"BACKGROUND",
+421
View File
@@ -0,0 +1,421 @@
//! Attaching drawing references: ATTACH / XATTACH (file picker, then the
//! Attach External Reference dialog), the -XREF command line, the `?` list,
//! and the commit that creates the definition and its INSERT as one undo step.
use super::*;
use crate::io::xref_model::Pathtype;
use crate::modules::insert::xattach::{
existing_reference, path_to_block_name, prepare_xref_definition, stored_path,
XAttachCommand, XrefAttachRequest, XrefPlacement,
};
use crate::modules::insert::xref_cmd::display_path;
use crate::ui::window::xref_attach::{XrefAttachMsg, XrefAttachState};
impl OpenCADStudio {
/// Commands owned here; `None` defers to the other families.
pub(super) fn dispatch_xref_attach(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
let upper = cmd.to_ascii_uppercase();
let (verb, rest) = match cmd.split_once(char::is_whitespace) {
Some((verb, rest)) => (verb.to_ascii_uppercase(), rest.trim()),
None => (upper.clone(), ""),
};
match (verb.as_str(), rest.is_empty()) {
("ATTACH", true) => Some(Task::done(Message::AttachPick)),
("XATTACH", true) => Some(Task::done(Message::XAttachPick)),
// A file named on the command line skips the picker.
("ATTACH", false) => Some(Task::done(Message::AttachPickResult(Ok(
std::path::PathBuf::from(rest.trim_matches('"')),
)))),
("XATTACH", false) => {
self.open_xref_attach_dialog(std::path::PathBuf::from(rest.trim_matches('"')));
Some(Task::none())
}
// XREF opens the External References palette; -XREF asks on the
// command line. Both keep their argument forms for scripts.
("XREF", true) | ("XREFSHOW", true) => {
if !self.show_external_references {
return self.dispatch_blocks("EXTERNALREFERENCES", i);
}
self.refresh_xref_manager();
Some(self.finish_dispatch(cmd))
}
("-XREF", true) => {
let saved: Vec<(String, String)> = self.tabs[i]
.scene
.document
.block_records
.iter()
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.map(|br| (br.name.clone(), br.xref_path.clone()))
.collect();
let command = crate::modules::insert::xref_cmd::XrefCommand::new(saved);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
Some(Task::none())
}
("XREFLIST", _) => {
self.list_xrefs(i, if rest.is_empty() { "*" } else { rest });
Some(self.finish_dispatch(cmd))
}
("XREFATTACHFILE", false) | ("XREFOVERLAYFILE", false) => {
let request = XrefAttachRequest {
path: rest.trim_matches('"').to_string(),
overlay: verb == "XREFOVERLAYFILE",
// The reference default (REFPATHTYPE 1): relative once the
// drawing is saved, the full path before that.
path_type: Pathtype::Relative,
};
self.start_xref_attach(i, request, XrefPlacement::on_screen(), "-XREF");
Some(Task::none())
}
_ => None,
}
}
/// `-XREF ?` — the reference table as the reference application prints it.
fn list_xrefs(&mut self, i: usize, pattern: &str) {
let rows: Vec<(String, &'static str, String)> = self.tabs[i]
.scene
.document
.block_records
.iter()
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.filter(|br| {
pattern == "*" || crate::io::xref_model::wildcard_match(&br.name, pattern)
})
.map(|br| {
let kind = if br.flags.is_xref_overlay { "Overlay" } else { "Attach" };
(br.name.clone(), kind, display_path(&br.xref_path))
})
.collect();
let out = &mut self.command_line;
out.push_output("");
out.push_output(&format!(
"{:<34}{:<13}{}",
"Xref name",
"Xref Type",
"Path"
));
out.push_output(&format!("{:<34}{:<13}{}", "----------------------", "---------", "----------"));
out.push_output("");
for (name, kind, path) in &rows {
out.push_output(&format!("{:<34}{:<13}{}", format!("\"{name}\""), *kind, path));
}
out.push_output("");
out.push_output(&format!("Total Xref(s): {}", rows.len()));
}
/// Checks the file, reports it the way the reference does, then asks
/// for whatever is left on-screen (or commits straight away).
pub(in crate::app) fn start_xref_attach(
&mut self,
i: usize,
request: XrefAttachRequest,
placement: XrefPlacement,
label: &'static str,
) {
let name = path_to_block_name(&request.path);
let full = crate::modules::insert::xattach::resolve_xref_store_path(&request.path, None);
let host = self.tabs[i].current_path.clone();
if let Some(host) = host.as_deref() {
if crate::modules::insert::xattach::is_self_attach(host, &full, host.parent()) {
self.command_line.push_error(
"Error: Possible circular reference to current drawing. *Invalid*",
);
return;
}
}
if let Some(existing) = existing_reference(&self.tabs[i].scene, &name) {
self.command_line
.push_output(&format!("Xref \"{}\" has already been defined.", existing));
self.command_line
.push_output("Using existing definition.");
} else {
let verb = if request.overlay {
format!("Overlay Xref \"{}\": {}", name, display_path(&full))
} else {
format!("Attach Xref \"{}\": {}", name, display_path(&full))
};
self.command_line.push_output(&verb);
if !std::path::Path::new(&full).is_file() {
let file = std::path::Path::new(&full.replace('\\', "/"))
.file_name()
.map(|f| f.to_string_lossy().into_owned())
.unwrap_or(full.clone());
self.command_line
.push_output(&format!("\"{}\" cannot be found.", file));
self.command_line.push_error("*Invalid*");
return;
}
self.command_line
.push_output(&format!("\"{}\" loaded.", name));
}
let command = XAttachCommand::new(request.clone(), placement, label);
if let Some(entity) = command.immediate() {
self.commit_xref_attach(i, request, entity);
return;
}
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
/// Creates (or reuses) the definition and commits its INSERT — one undo
/// step for both, so undo leaves no definition behind.
pub(in crate::app) fn commit_xref_attach(
&mut self,
i: usize,
request: XrefAttachRequest,
mut entity: acadrust::EntityType,
) {
let pending = self.begin_undo(i, "XATTACH", 1, false);
let host = self.tabs[i].current_path.clone();
let name = prepare_xref_definition(&mut self.tabs[i].scene, &request, host.as_deref());
if let acadrust::EntityType::Insert(insert) = &mut entity {
insert.block_name = name;
}
// Resolving merged the reference's layers and linetypes — mirror
// them into the Layers panel and ribbon dropdowns now (#407).
self.refresh_layer_panel();
let _ = self.commit_entity_handle(entity);
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
if let Some(pending) = pending {
self.commit_undo_delta(i, pending);
}
self.refresh_xref_manager();
}
/// Opens the Attach External Reference dialog for `path`.
pub(in crate::app) fn open_xref_attach_dialog(&mut self, path: std::path::PathBuf) {
let i = self.active_tab;
let existing: Vec<(String, String)> = self.tabs[i]
.scene
.document
.block_records
.iter()
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.map(|br| (br.name.clone(), br.xref_path.clone()))
.collect();
let mut state = XrefAttachState::new(existing);
self.load_xref_attach_file(&mut state, &path.to_string_lossy());
self.xref_attach = Some(state);
self.active_modal = Some(crate::app::ModalKind::XrefAttach);
}
/// Fills the dialog from the chosen file: name, preview, block unit and
/// the paths shown under Details.
fn load_xref_attach_file(&self, state: &mut XrefAttachState, path: &str) {
let i = self.active_tab;
state.path = path.to_string();
state.name = path_to_block_name(path);
state.found_in = display_path(path);
#[cfg(not(target_arch = "wasm32"))]
{
state.preview = crate::io::thumbnail::read_handle(std::path::Path::new(path));
let host_units = self.tabs[i].scene.document.header.insertion_units;
let source_units = crate::io::load_file(std::path::Path::new(path))
.map(|doc| doc.header.insertion_units)
.unwrap_or(0);
state.unit_label = crate::t!(crate::app::properties::insunits_name(source_units))
.into_owned();
let factor =
crate::app::properties::insert_unit_scale(host_units, source_units).unwrap_or(1.0);
state.unit_factor = crate::app::properties::format_unit_factor(factor);
}
self.refresh_xref_attach_saved_path(state);
}
fn refresh_xref_attach_saved_path(&self, state: &mut XrefAttachState) {
let host = self.tabs[self.active_tab].current_path.clone();
let request = XrefAttachRequest {
path: state.path.clone(),
overlay: state.overlay,
path_type: state.path_type.0,
};
state.saved_path = display_path(&stored_path(&request, host.as_deref()));
}
/// Messages of the ATTACH picker and the dialog.
pub(in crate::app) fn update_xref_attach(&mut self, message: Message) -> Task<Message> {
match message {
Message::AttachPick => Task::perform(
async {
let handle = crate::sys::file_dialog()
.set_title(crate::t!("Select Reference File").as_ref())
.add_filter(
crate::t!("All Reference Files").as_ref(),
&[
"dwg", "dxf", "pdf", "png", "jpg", "jpeg", "bmp", "tif", "tiff",
],
)
.add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"])
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf"])
.add_filter(
crate::t!("Images").as_ref(),
&["png", "jpg", "jpeg", "bmp", "tif", "tiff"],
)
.pick_file()
.await;
match handle {
Some(h) => Ok(crate::sys::handle_path(&h)),
None => Err("Cancelled".to_string()),
}
},
Message::AttachPickResult,
),
Message::AttachPickResult(Ok(path)) => {
let ext = path
.extension()
.map(|e| e.to_string_lossy().to_ascii_lowercase())
.unwrap_or_default();
match ext.as_str() {
"pdf" => match std::fs::read(&path) {
Ok(bytes) => self.update(Message::PdfAttachPickResult(Ok((
path,
std::sync::Arc::new(bytes),
)))),
Err(e) => self.update(Message::PdfAttachPickResult(Err(e.to_string()))),
},
"png" | "jpg" | "jpeg" | "bmp" | "tif" | "tiff" => {
let result = image::image_dimensions(&path)
.map(|(w, h)| (path.clone(), w, h))
.map_err(|e| e.to_string());
self.update(Message::ImagePickResult(result))
}
_ => {
self.open_xref_attach_dialog(path);
Task::none()
}
}
}
Message::AttachPickResult(Err(e)) => {
if e != "Cancelled" {
self.command_line
.push_error(format!("ATTACH: {e}").as_ref());
}
Task::none()
}
Message::XrefAttachBrowseResult(Ok(path)) => {
if let Some(mut state) = self.xref_attach.take() {
self.load_xref_attach_file(&mut state, &path.to_string_lossy());
self.xref_attach = Some(state);
}
Task::none()
}
Message::XrefAttachBrowseResult(Err(_)) => Task::none(),
Message::XrefAttach(edit) => self.edit_xref_attach(edit),
_ => Task::none(),
}
}
fn edit_xref_attach(&mut self, edit: XrefAttachMsg) -> Task<Message> {
let Some(mut state) = self.xref_attach.take() else {
return Task::none();
};
let mut task = Task::none();
match edit {
XrefAttachMsg::Name(name) => {
if let Some((_, saved)) = state
.existing
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(&name))
.cloned()
{
let host = self.tabs[self.active_tab].current_path.clone();
let base = host.as_deref().and_then(|h| h.parent());
let full = crate::modules::insert::xattach::resolve_xref_store_path(&saved, base);
self.load_xref_attach_file(&mut state, &full);
}
}
XrefAttachMsg::Browse => {
task = Task::perform(
async {
let handle = crate::sys::file_dialog()
.set_title(crate::t!("Select Reference File").as_ref())
.add_filter(crate::t!("Drawing").as_ref(), &["dwg", "dxf"])
.pick_file()
.await;
match handle {
Some(h) => Ok(crate::sys::handle_path(&h)),
None => Err("Cancelled".to_string()),
}
},
Message::XrefAttachBrowseResult,
);
}
XrefAttachMsg::Overlay(overlay) => state.overlay = overlay,
XrefAttachMsg::ScaleOnScreen(on) => state.scale_on_screen = on,
XrefAttachMsg::Scale(axis, value) => {
if axis == 0 && state.uniform {
state.scale = [value.clone(), value.clone(), value];
} else if let Some(slot) = state.scale.get_mut(axis) {
*slot = value;
}
}
XrefAttachMsg::Uniform(on) => {
state.uniform = on;
if on {
let x = state.scale[0].clone();
state.scale = [x.clone(), x.clone(), x];
}
}
XrefAttachMsg::InsertOnScreen(on) => state.insert_on_screen = on,
XrefAttachMsg::Insert(axis, value) => {
if let Some(slot) = state.insert.get_mut(axis) {
*slot = value;
}
}
XrefAttachMsg::PathType(choice) => {
state.path_type = choice;
self.refresh_xref_attach_saved_path(&mut state);
}
XrefAttachMsg::RotationOnScreen(on) => state.rotation_on_screen = on,
XrefAttachMsg::Rotation(value) => state.rotation = value,
XrefAttachMsg::Details(on) => state.details = on,
XrefAttachMsg::Help => {
self.command_line.push_info(
crate::t!("Inserts references to external files such as other drawings, raster images, and underlays.")
.as_ref(),
);
}
XrefAttachMsg::Apply => {
let placement = XrefPlacement {
insert: (!state.insert_on_screen)
.then(|| state.insert_point())
.flatten(),
scale: (!state.scale_on_screen).then(|| state.scale_values()).flatten(),
rotation: (!state.rotation_on_screen)
.then(|| crate::entities::common::parse_typed_angle(&state.rotation))
.flatten()
.map(f64::to_degrees),
};
let invalid = (!state.insert_on_screen && placement.insert.is_none())
|| (!state.scale_on_screen
&& placement
.scale
.is_none_or(|s| s.iter().any(|v| *v == 0.0)))
|| (!state.rotation_on_screen && placement.rotation.is_none());
if invalid || state.path.is_empty() {
self.command_line
.push_error(crate::t!("Invalid input.").as_ref());
self.xref_attach = Some(state);
return Task::none();
}
let request = XrefAttachRequest {
path: state.path.clone(),
overlay: state.overlay,
path_type: state.path_type.0,
};
self.active_modal = None;
let i = self.active_tab;
self.start_xref_attach(i, request, placement, "XATTACH");
return Task::none();
}
}
self.xref_attach = Some(state);
task
}
}