From b15be3af3d0d50545d307bf0cc46fac01008fb19 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 8 Apr 2026 00:05:04 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20WBLOCK=20=E2=80=94=20write=20block=20or?= =?UTF-8?q?=20selected=20entities=20to=20external=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/modules/insert/wblock.rs: extract_block_to_doc() copies a named block's entities into a fresh CadDocument; extract_entities_to_doc() does the same for a selection set; layer definitions are carried over - WBLOCK writes the named block; WBLOCK * writes selected entities - WblockSave / WblockSaveResult messages drive the async save-dialog flow - ROADMAP.md: 1.10 WBLOCK marked ✅ Co-Authored-By: Claude Sonnet 4.6 --- ROADMAP.md | 4 +- src/app/commands.rs | 17 ++++++ src/app/mod.rs | 5 ++ src/app/update.rs | 44 +++++++++++++++ src/modules/insert/mod.rs | 2 + src/modules/insert/wblock.rs | 106 +++++++++++++++++++++++++++++++++++ 6 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 src/modules/insert/wblock.rs diff --git a/ROADMAP.md b/ROADMAP.md index bc282da05..2edb13792 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,7 +19,7 @@ Durum simgeleri: ✅ Tamamlandı · 🔧 Kısmen yapıldı · ⬜ Yapılmadı | 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 | ✅ Auto-resolve on open, XATTACH/XREF/XRELOAD commands | -| 1.10 | WBLOCK — bloğu dış dosyaya yazma | ⬜ | +| 1.10 | WBLOCK — bloğu dış dosyaya yazma | ✅ Block name or selected entities → DWG/DXF | | 1.11 | Serde entegrasyonu (JSON/alternatif I/O) | ⬜ | | 1.12 | Bozuk DWG kurtarma (failsafe parse) | ⬜ | @@ -226,7 +226,7 @@ Underlay (PDF/DWF/DGN) | 3D Cylinder primitive | ✅ | | OBJ dosyası içe aktarma | ✅ | | REFEDIT (block yerinde düzenleme) | ⬜ | -| WBLOCK (bloğu dış dosyaya yaz) | ⬜ | +| WBLOCK (bloğu dış dosyaya yaz) | ✅ | | Attributeli INSERT akışı (ATTREQ) | ⬜ | --- diff --git a/src/app/commands.rs b/src/app/commands.rs index 1d483e090..d7b69b9da 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -422,6 +422,23 @@ impl H7CAD { return 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() { + // No argument: use selected entities (*) if any, else ask. + let sel: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect(); + if sel.is_empty() { + self.command_line.push_error( + "WBLOCK Select entities first, or: WBLOCK or WBLOCK *", + ); + } else { + return Task::done(Message::WblockSave("*".to_string())); + } + } else { + return Task::done(Message::WblockSave(arg.to_string())); + } + } + "XREF" | "XR" => { // List all xref blocks in the current drawing. let xrefs: Vec = self.tabs[i] diff --git a/src/app/mod.rs b/src/app/mod.rs index ac140e090..d74822f78 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -417,6 +417,11 @@ pub enum Message { XAttachPick, /// Result of the XATTACH file picker. XAttachPickResult(Result), + // ── WBLOCK ──────────────────────────────────────────────────────────── + /// Trigger the WBLOCK save dialog for `block_name` (or `*` = selection). + WblockSave(String), + /// Result of the WBLOCK save path dialog. + WblockSaveResult(String, Option), } impl H7CAD { diff --git a/src/app/update.rs b/src/app/update.rs index 3ce6557eb..25b8ed0a7 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -163,6 +163,50 @@ impl H7CAD { Task::none() } + Message::WblockSave(block_name) => { + let name = block_name.clone(); + Task::perform( + async move { + let path = crate::io::pick_save_path().await; + (name, path) + }, + |(name, path)| Message::WblockSaveResult(name, path), + ) + } + + Message::WblockSaveResult(block_name, Some(path)) => { + let i = self.active_tab; + let result = if block_name == "*" { + let handles: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect(); + crate::modules::insert::wblock::extract_entities_to_doc( + &self.tabs[i].scene.document, + &handles, + ) + } else { + crate::modules::insert::wblock::extract_block_to_doc( + &self.tabs[i].scene.document, + &block_name, + ) + }; + match result { + Ok(doc) => match crate::io::save(&doc, &path) { + Ok(()) => { + let fname = path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| path.to_string_lossy().into_owned()); + self.command_line + .push_output(&format!("WBLOCK Saved \"{block_name}\" → \"{fname}\"")); + } + Err(e) => self.command_line.push_error(&format!("WBLOCK save failed: {e}")), + }, + Err(e) => self.command_line.push_error(&format!("WBLOCK: {e}")), + } + Task::none() + } + + Message::WblockSaveResult(_, None) => Task::none(), + Message::SaveFile => { let i = self.active_tab; if let Some(path) = &self.tabs[i].current_path { diff --git a/src/modules/insert/mod.rs b/src/modules/insert/mod.rs index e2dcc30a8..e738f86b8 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 wblock; pub(crate) mod xattach; use crate::modules::{CadModule, RibbonGroup}; @@ -40,6 +41,7 @@ impl CadModule for InsertModule { tools: vec![ create_block::tool().into(), insert_block::tool().into(), + wblock::tool().into(), xattach::tool().into(), ], }, diff --git a/src/modules/insert/wblock.rs b/src/modules/insert/wblock.rs new file mode 100644 index 000000000..65fb59f62 --- /dev/null +++ b/src/modules/insert/wblock.rs @@ -0,0 +1,106 @@ +// WBLOCK — write a block (or selected entities) to an external DWG/DXF file. +// +// Two modes: +// block name → copies the named block definition to a new document +// * → copies currently selected model-space entities + +use acadrust::{CadDocument, EntityType}; + +use crate::modules::{IconKind, ModuleEvent, ToolDef}; + +pub fn tool() -> ToolDef { + ToolDef { + id: "WBLOCK", + label: "Write Block", + icon: IconKind::Svg(include_bytes!("../../../assets/icons/blocks/insert.svg")), + event: ModuleEvent::Command("WBLOCK".to_string()), + } +} + +/// Build a standalone `CadDocument` containing the named block's entities +/// extracted into model space. +/// +/// Returns `Err` if the block is not found or has no entities. +pub fn extract_block_to_doc( + src: &CadDocument, + block_name: &str, +) -> Result { + let br = src + .block_records + .get(block_name) + .ok_or_else(|| format!("Block \"{block_name}\" not found."))?; + + let handles = br.entity_handles.clone(); + if handles.is_empty() { + return Err(format!("Block \"{block_name}\" has no entities.")); + } + + let mut out = CadDocument::new(); + // Copy layers referenced by the block entities. + for h in &handles { + if let Some(e) = src.get_entity(*h) { + let layer = e.common().layer.clone(); + if !layer.is_empty() && !layer.eq("0") && out.layers.get(&layer).is_none() { + if let Some(src_layer) = src.layers.get(&layer) { + let _ = out.layers.add(src_layer.clone()); + } + } + } + } + + for h in handles { + if let Some(entity) = src.get_entity(h) { + if matches!(entity, EntityType::Block(_) | EntityType::BlockEnd(_)) { + continue; + } + let mut clone = entity.clone(); + clone.common_mut().handle = acadrust::types::Handle::NULL; + clone.common_mut().owner_handle = acadrust::types::Handle::NULL; + let _ = out.add_entity(clone); + } + } + + if out.entities().count() == 0 { + return Err(format!("Block \"{block_name}\" produced no exportable entities.")); + } + + Ok(out) +} + +/// Build a standalone `CadDocument` from an explicit list of entity handles +/// (the "selected entities" mode, `*`). +pub fn extract_entities_to_doc( + src: &CadDocument, + handles: &[acadrust::Handle], +) -> Result { + if handles.is_empty() { + return Err("No entities selected for WBLOCK.".into()); + } + + let mut out = CadDocument::new(); + + for &h in handles { + if let Some(entity) = src.get_entity(h) { + if matches!(entity, EntityType::Block(_) | EntityType::BlockEnd(_)) { + continue; + } + // Copy layer definition. + let layer = entity.common().layer.clone(); + if !layer.is_empty() && !layer.eq("0") && out.layers.get(&layer).is_none() { + if let Some(src_layer) = src.layers.get(&layer) { + let _ = out.layers.add(src_layer.clone()); + } + } + let mut clone = entity.clone(); + clone.common_mut().handle = acadrust::types::Handle::NULL; + clone.common_mut().owner_handle = acadrust::types::Handle::NULL; + let _ = out.add_entity(clone); + } + } + + if out.entities().count() == 0 { + return Err("None of the selected entities could be exported.".into()); + } + + Ok(out) +}