From f80d694818952fbf256b6c0bf74e7aaec104523e Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 8 Apr 2026 00:07:58 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20DATAEXTRACTION=20=E2=80=94=20export=20e?= =?UTF-8?q?ntity=20data=20to=20CSV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DATAEXTRACTION / EATTEXT / ATTEXT commands trigger an async save dialog - Extracts all model-space entities with: Type, Handle, Layer, Color, Linetype, and a geometry summary per entity type (coords, radius, etc.) - DataExtractionSave / DataExtractionSaveResult message pair drives the rfd save-file dialog asynchronously - CSV fields are properly escaped (commas, quotes, newlines) - ROADMAP.md: 12 DATAEXTRACTION marked ✅ Co-Authored-By: Claude Sonnet 4.6 --- ROADMAP.md | 2 +- src/app/commands.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++ src/app/mod.rs | 5 ++++ src/app/update.rs | 39 ++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 2edb13792..8d684ac50 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -258,7 +258,7 @@ Underlay (PDF/DWF/DGN) | QSELECT — özelliğe göre seç | ✅ | | FLATTEN (Z=0 düzleme) | ✅ | | MASSPROP (alan merkezi, atalet) | ✅ | -| DATAEXTRACTION | ⬜ | +| DATAEXTRACTION | ✅ CSV export: type/handle/layer/color/linetype/geometry | --- diff --git a/src/app/commands.rs b/src/app/commands.rs index d7b69b9da..b4fe6ecca 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -1612,6 +1612,11 @@ impl H7CAD { self.command_line.push_output(&format!("COUNT: {total} entity(ies) total.")); } + "DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => { + let csv = build_data_extraction_csv(&self.tabs[i].scene.document); + return Task::done(Message::DataExtractionSave(csv)); + } + // ── Find / Replace ──────────────────────────────────────────────── // FIND — list all Text/MText/Dimension containing // FIND REPLACE — replace first occurrence (case-insensitive) @@ -3613,3 +3618,71 @@ fn replace_entity_text(entity: &mut acadrust::EntityType, search: &str, rep: &st } } + +// ── DATAEXTRACTION ───────────────────────────────────────────────────────── + +/// Build a CSV string with one row per entity in model space. +/// Columns: Type, Handle, Layer, Color, Linetype, ExtraInfo +fn build_data_extraction_csv(doc: &acadrust::CadDocument) -> String { + use acadrust::EntityType; + + let mut out = String::from("Type,Handle,Layer,Color,Linetype,ExtraInfo\n"); + + let ms_handle = doc.header.model_space_block_handle; + for e in doc.entities() { + // Skip Block/EndBlock sentinels and paper-space entities. + if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) { + continue; + } + if !ms_handle.is_null() && e.common().owner_handle != ms_handle { + continue; + } + let type_name = entity_type_name(e); + let handle = format!("{:X}", e.common().handle.value()); + let layer = csv_escape(&e.common().layer); + let color = format!("{}", e.common().color); + let lt = csv_escape(&e.common().linetype); + let extra = csv_escape(&entity_extra_info(e)); + out.push_str(&format!("{type_name},{handle},{layer},{color},{lt},{extra}\n")); + } + out +} + +/// Return a short geometry summary for CSV ExtraInfo column. +fn entity_extra_info(entity: &acadrust::EntityType) -> String { + use acadrust::EntityType; + match entity { + EntityType::Line(e) => format!( + "({:.3},{:.3})-({:.3},{:.3})", + e.start.x, e.start.y, e.end.x, e.end.y + ), + EntityType::Circle(e) => format!( + "C({:.3},{:.3}) R={:.3}", + e.center.x, e.center.y, e.radius + ), + EntityType::Arc(e) => format!( + "C({:.3},{:.3}) R={:.3} {:.1}°-{:.1}°", + e.center.x, e.center.y, e.radius, e.start_angle, e.end_angle + ), + EntityType::Text(e) => e.value.clone(), + EntityType::MText(e) => e.value.chars().take(60).collect(), + EntityType::Insert(e) => format!("BLK={} @({:.3},{:.3})", e.block_name, e.insert_point.x, e.insert_point.y), + EntityType::LwPolyline(e) => format!("{} vertices", e.vertices.len()), + EntityType::Polyline(e) => format!("{} vertices", e.vertices.len()), + EntityType::Polyline2D(e) => format!("{} vertices", e.vertices.len()), + EntityType::Polyline3D(e) => format!("{} vertices", e.vertices.len()), + EntityType::Hatch(e) => format!("PAT={}", e.pattern.name), + EntityType::Dimension(e) => format!("{:.3}", e.base().actual_measurement), + EntityType::Spline(e) => format!("{} ctrl pts", e.control_points.len()), + _ => String::new(), + } +} + +/// Escape a string for a CSV field (wrap in quotes if it contains comma/quote/newline). +fn csv_escape(s: &str) -> String { + if s.contains(',') || s.contains('"') || s.contains('\n') { + format!("\"{}\"", s.replace('"', "\"\"")) + } else { + s.to_string() + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index d74822f78..33eb21903 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -422,6 +422,11 @@ pub enum Message { WblockSave(String), /// Result of the WBLOCK save path dialog. WblockSaveResult(String, Option), + // ── DATAEXTRACTION ──────────────────────────────────────────────────── + /// Save the pre-built CSV string to a file chosen by the user. + DataExtractionSave(String), + /// Path chosen (or None = cancelled). + DataExtractionSaveResult(String, Option), } impl H7CAD { diff --git a/src/app/update.rs b/src/app/update.rs index 25b8ed0a7..00e036acd 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -207,6 +207,45 @@ impl H7CAD { Message::WblockSaveResult(_, None) => Task::none(), + Message::DataExtractionSave(csv) => { + let csv_clone = csv.clone(); + Task::perform( + async move { + let path = rfd::AsyncFileDialog::new() + .set_title("Save Data Extraction") + .set_file_name("extraction.csv") + .add_filter("CSV", &["csv"]) + .add_filter("All Files", &["*"]) + .save_file() + .await + .map(|h| h.path().to_path_buf()); + (csv_clone, path) + }, + |(csv, path)| Message::DataExtractionSaveResult(csv, path), + ) + } + + Message::DataExtractionSaveResult(csv, Some(path)) => { + match std::fs::write(&path, csv.as_bytes()) { + Ok(()) => { + let rows = csv.lines().count().saturating_sub(1); + 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!( + "DATAEXTRACTION {rows} rows → \"{fname}\"" + )); + } + Err(e) => self + .command_line + .push_error(&format!("DATAEXTRACTION: write failed: {e}")), + } + Task::none() + } + + Message::DataExtractionSaveResult(_, None) => Task::none(), + Message::SaveFile => { let i = self.active_tab; if let Some(path) = &self.tabs[i].current_path {