feat: DATAEXTRACTION — export entity data to CSV
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
b15be3af3d
commit
f80d694818
4 changed files with 118 additions and 1 deletions
|
|
@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <search> — list all Text/MText/Dimension containing <search>
|
||||
// FIND <search> REPLACE <rep> — 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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -422,6 +422,11 @@ pub enum Message {
|
|||
WblockSave(String),
|
||||
/// Result of the WBLOCK save path dialog.
|
||||
WblockSaveResult(String, Option<std::path::PathBuf>),
|
||||
// ── DATAEXTRACTION ────────────────────────────────────────────────────
|
||||
/// Save the pre-built CSV string to a file chosen by the user.
|
||||
DataExtractionSave(String),
|
||||
/// Path chosen (or None = cancelled).
|
||||
DataExtractionSaveResult(String, Option<std::path::PathBuf>),
|
||||
}
|
||||
|
||||
impl H7CAD {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue