From 64837e9aedbef1d867f132c4a974de791541b928 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 8 Apr 2026 00:02:38 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20XREF=20management=20=E2=80=94=20auto-re?= =?UTF-8?q?solve,=20XATTACH,=20XREF,=20XRELOAD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- ROADMAP.md | 2 +- src/app/cmd_result.rs | 21 ++++ src/app/commands.rs | 68 +++++++++++++ src/app/mod.rs | 5 + src/app/update.rs | 62 +++++++++++- src/command/mod.rs | 6 ++ src/io/mod.rs | 1 + src/io/xref.rs | 143 +++++++++++++++++++++++++++ src/modules/insert/mod.rs | 7 +- src/modules/insert/xattach.rs | 175 ++++++++++++++++++++++++++++++++++ 10 files changed, 487 insertions(+), 3 deletions(-) create mode 100644 src/io/xref.rs create mode 100644 src/modules/insert/xattach.rs diff --git a/ROADMAP.md b/ROADMAP.md index 9a1904ec..bc282da0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -18,7 +18,7 @@ Durum simgeleri: βœ… TamamlandΔ± Β· πŸ”§ KΔ±smen yapΔ±ldΔ± Β· ⬜ YapΔ±lmadΔ± | 1.6 | Undo / Redo (snapshot stack) | βœ… | | 1.7 | PDF dışa aktarma (CTB/STB plot style) | βœ… | | 1.8 | Fiziksel yazΔ±cΔ±ya yazdΔ±rma | ⬜ | -| 1.9 | XREF (dış referans) yΓΆnetimi | ⬜ | +| 1.9 | XREF (dış referans) yΓΆnetimi | βœ… Auto-resolve on open, XATTACH/XREF/XRELOAD commands | | 1.10 | WBLOCK β€” bloğu dış dosyaya yazma | ⬜ | | 1.11 | Serde entegrasyonu (JSON/alternatif I/O) | ⬜ | | 1.12 | Bozuk DWG kurtarma (failsafe parse) | ⬜ | diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 2b779867..ea8133a5 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -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 = { + 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); diff --git a/src/app/commands.rs b/src/app/commands.rs index 012a3d1e..1d483e09 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -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 = 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; diff --git a/src/app/mod.rs b/src/app/mod.rs index 1fceb032..ac140e09 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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), } impl H7CAD { diff --git a/src/app/update.rs b/src/app/update.rs index 21272b7f..3ce6557e 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -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 { diff --git a/src/command/mod.rs b/src/command/mod.rs index e66ca4c1..7bfb20c8 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -227,6 +227,12 @@ pub trait CadCommand: Send { false } + /// If this command is XATTACH, returns the file path to attach. + /// Default: None. + fn xattach_path(&self) -> Option { + None + } + /// Called instead of `on_point` when the command needs a tangent pick /// and the snap system found a tangent object. fn on_tangent_point(&mut self, obj: TangentObject, hit: Vec3) -> CmdResult { diff --git a/src/io/mod.rs b/src/io/mod.rs index bc7d3043..a51ecc31 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -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}; diff --git a/src/io/xref.rs b/src/io/xref.rs new file mode 100644 index 00000000..313e0cf5 --- /dev/null +++ b/src/io/xref.rs @@ -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 { + // 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 { + 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 = 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; +} diff --git a/src/modules/insert/mod.rs b/src/modules/insert/mod.rs index 444479eb..e2dcc30a 100644 --- a/src/modules/insert/mod.rs +++ b/src/modules/insert/mod.rs @@ -7,6 +7,7 @@ mod cylinder; pub(crate) mod insert_block; mod open_obj; mod sphere; +pub(crate) mod xattach; use crate::modules::{CadModule, RibbonGroup}; @@ -36,7 +37,11 @@ impl CadModule for InsertModule { }, RibbonGroup { title: "Block", - tools: vec![create_block::tool().into(), insert_block::tool().into()], + tools: vec![ + create_block::tool().into(), + insert_block::tool().into(), + xattach::tool().into(), + ], }, ] } diff --git a/src/modules/insert/xattach.rs b/src/modules/insert/xattach.rs new file mode 100644 index 00000000..acc0cab3 --- /dev/null +++ b/src/modules/insert/xattach.rs @@ -0,0 +1,175 @@ +// XATTACH command β€” attach an external DWG/DXF file as an XREF block +// and insert it at a picked point. +// +// Workflow: +// Step 1 (text input): user types the file path (or the file-picker +// message has already supplied it). +// Step 2 (point pick): user clicks the insertion point. +// Result: BlockRecord + Block entities are created with is_xref=true, +// then an INSERT entity is committed. + +use acadrust::entities::{Block, BlockEnd, Insert}; +use acadrust::tables::block_record::{BlockFlags, BlockRecord}; +use acadrust::types::Vector3; +use acadrust::EntityType; +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::modules::{IconKind, ModuleEvent, ToolDef}; +use crate::scene::wire_model::WireModel; +use crate::scene::Scene; + +pub fn tool() -> ToolDef { + ToolDef { + id: "XATTACH", + label: "Attach XREF", + icon: IconKind::Svg(include_bytes!("../../../assets/icons/blocks/insert.svg")), + event: ModuleEvent::Command("XATTACH".to_string()), + } +} + +#[allow(dead_code)] +enum Step { + /// Waiting for the user to type (or accept) a file path. + FilePath, + /// File path is confirmed; waiting for an insertion point. + InsertionPoint { path: String, block_name: String }, +} + +pub struct XAttachCommand { + step: Step, + /// Pre-supplied path (from the file-picker message). + prefilled_path: Option, +} + +impl XAttachCommand { + #[allow(dead_code)] + pub fn new() -> Self { + Self { + step: Step::FilePath, + prefilled_path: None, + } + } + + /// Create an XATTACH command with a path already filled in (from file-picker). + pub fn with_path(path: String) -> Self { + let block_name = path_to_block_name(&path); + Self { + step: Step::InsertionPoint { + path: path.clone(), + block_name, + }, + prefilled_path: Some(path), + } + } +} + +impl CadCommand for XAttachCommand { + fn name(&self) -> &'static str { + "XATTACH" + } + + fn prompt(&self) -> String { + match &self.step { + Step::FilePath => "XATTACH Enter path to external DWG/DXF file:".to_string(), + Step::InsertionPoint { block_name, .. } => { + format!("XATTACH Specify insertion point for \"{}\":", block_name) + } + } + } + + fn wants_text_input(&self) -> bool { + matches!(self.step, Step::FilePath) + } + + fn on_text_input(&mut self, text: &str) -> Option { + if !matches!(self.step, Step::FilePath) { + return None; + } + let path = text.trim().to_string(); + if path.is_empty() { + return None; + } + let block_name = path_to_block_name(&path); + self.step = Step::InsertionPoint { path, block_name }; + Some(CmdResult::NeedPoint) + } + + fn on_point(&mut self, pt: Vec3) -> CmdResult { + match &self.step { + Step::FilePath => CmdResult::NeedPoint, + Step::InsertionPoint { path: _, block_name } => { + // We return the INSERT entity; the command handler in + // commands.rs will call `prepare_xref_block` on the scene + // before committing. + CmdResult::CommitAndExit(EntityType::Insert(Insert::new( + block_name.clone(), + Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64), + ))) + } + } + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_preview_wires(&mut self, _pt: Vec3) -> Vec { + vec![] + } + + fn xattach_path(&self) -> Option { + match &self.step { + Step::InsertionPoint { path, .. } => Some(path.clone()), + Step::FilePath => self.prefilled_path.clone(), + } + } +} + +/// Derive a block name from the file path: take the file stem, uppercase it. +pub fn path_to_block_name(path: &str) -> String { + let p = std::path::Path::new(path); + p.file_stem() + .map(|s| s.to_string_lossy().to_uppercase()) + .unwrap_or_else(|| "XREF".to_string()) +} + +/// Create the XREF BlockRecord + Block/EndBlock entities in the scene document +/// for a given file path. Returns the block name. +/// +/// This must be called before committing the INSERT so that the block +/// definition exists when the renderer looks it up. +pub fn prepare_xref_block(scene: &mut Scene, path: &str) -> String { + let block_name = path_to_block_name(path); + + // If a BlockRecord already exists with this name, skip creation. + if scene.document.block_records.get(&block_name).is_some() { + return block_name; + } + + // Create the BlockRecord. + let mut br = BlockRecord::new(&block_name); + br.handle = scene.document.allocate_handle(); + br.flags = BlockFlags { + is_xref: true, + is_xref_overlay: false, + anonymous: false, + has_attributes: false, + is_external: false, + }; + br.xref_path = path.to_string(); + let _ = scene.document.block_records.add(br); + + // Create BLOCK entity. + let b = Block::new(&block_name, Vector3::zero()).with_xref_path(path); + let _ = scene.document.add_entity(EntityType::Block(b)); + let _ = scene.document.add_entity(EntityType::BlockEnd(BlockEnd::new())); + + // Resolve the XREF content immediately. + let path_buf = std::path::PathBuf::from(path); + if let Some(base_dir) = path_buf.parent() { + crate::io::xref::resolve_xrefs(&mut scene.document, base_dir); + } + + block_name +}