fix(annotate): complete table and data workflows

This commit is contained in:
ramox81 committed 2026-09-14 14:54:55 +03:00
1 parent 6505b27818
commit a35c996f9f
17 files changed
+2481 -145

No files matched your search

File diff suppressed because it is too large. Load diff
+5 -2
View File
@@ -2474,6 +2474,7 @@ impl OpenCADStudio {
CmdResult::CommitManyAndEditText {
entities,
edit_index,
open_editor,
} => {
let label = self.history_label_from_active_cmd(i, "ENTITY");
let delta_safe = entities
@@ -2527,8 +2528,10 @@ impl OpenCADStudio {
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
if let Some(h) = edit_handle {
return self.begin_text_edit(h);
if open_editor {
if let Some(h) = edit_handle {
return self.begin_text_edit(h);
}
}
}
CmdResult::CreateBlock {
+1 -26
View File
@@ -668,32 +668,7 @@ impl OpenCADStudio {
}
"TABLE" => {
use crate::modules::annotate::table_cmd::TableCommand;
let name = self.tabs[i]
.scene
.document
.header
.current_table_style_name
.clone();
let style = self.tabs[i]
.scene
.document
.objects
.iter()
.find_map(|(handle, object)| match object {
acadrust::objects::ObjectType::TableStyle(style)
if style.name.eq_ignore_ascii_case(&name) =>
{
Some((*handle, style.clone()))
}
_ => None,
});
let multiplier = self.tabs[i].scene.creation_annotation_multiplier();
let cmd = style.as_ref().map_or_else(TableCommand::new, |(handle, style)| {
TableCommand::with_style(*handle, style, multiplier)
});
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
self.open_table_insert();
}
"TABLEDIT" => {
+29 -30
View File
@@ -1585,10 +1585,7 @@ impl OpenCADStudio {
// DATALINK <path.csv> — create a persistent linked table.
"DATALINK" => {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new("DATALINK", "DATALINK path to the .csv file:");
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
self.open_data_link_manager(false);
}
cmd if cmd.starts_with("DATALINK ") => {
let path = cmd.trim_start_matches("DATALINK").trim();
@@ -1690,20 +1687,7 @@ impl OpenCADStudio {
let Some(link_handle) = link_handle else {
continue;
};
let path = match self.tabs[i].scene.document.objects.get(&link_handle) {
Some(acadrust::objects::ObjectType::ClassObject(object)) => {
match &object.data {
acadrust::objects::ClassObjectData::DataLink(link) => {
Some(link.connection_string.clone())
}
_ => None,
}
}
_ => None,
};
if let Some(path) = path {
jobs.push((*handle, link_handle, path));
}
jobs.push((*handle, link_handle));
}
if jobs.is_empty() {
self.command_line
@@ -1712,28 +1696,43 @@ impl OpenCADStudio {
}
if write_back {
let mut written = 0usize;
for (table_handle, _, path) in &jobs {
let mut errors = Vec::new();
for (table_handle, link_handle) in &jobs {
let Some(acadrust::EntityType::Table(table)) =
self.tabs[i].scene.document.get_entity(*table_handle)
else {
continue;
};
let csv = table_to_csv(table);
if std::fs::write(path, csv).is_ok() {
written += 1;
match crate::app::annotation_data::data_link_write_path(
&self.tabs[i].scene.document,
*link_handle,
)
.and_then(|path| std::fs::write(path, csv).map_err(|error| error.to_string()))
{
Ok(()) => written += 1,
Err(error) => errors.push(error),
}
}
self.command_line.push_output(
crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(),
);
if written > 0 {
self.command_line.push_output(
crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(),
);
}
for error in errors {
self.command_line.push_error(&error);
}
return Some(Task::none());
}
let updates: Vec<_> = jobs
.into_iter()
.filter_map(|(table_handle, link_handle, path)| {
std::fs::read_to_string(&path)
.ok()
.map(|text| (table_handle, link_handle, parse_csv_table(&text)))
.filter_map(|(table_handle, link_handle)| {
crate::app::annotation_data::read_data_link(
&self.tabs[i].scene.document,
link_handle,
)
.ok()
.map(|rows| (table_handle, link_handle, rows))
})
.filter(|(_, _, rows)| !rows.is_empty())
.collect();
@@ -1888,7 +1887,7 @@ fn parse_landxml_cgpoints(xml: &str) -> Vec<[f64; 3]> {
out
}
fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
pub(crate) fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
let mut rows = Vec::new();
let mut row = Vec::new();
let mut field = String::new();
@@ -1928,7 +1927,7 @@ fn parse_csv_table(text: &str) -> Vec<Vec<String>> {
rows
}
fn table_to_csv(table: &acadrust::entities::Table) -> String {
pub(crate) fn table_to_csv(table: &acadrust::entities::Table) -> String {
fn escape(value: &str) -> String {
if value.contains([',', '"', '\r', '\n']) {
format!("\"{}\"", value.replace('"', "\"\""))
+1 -78
View File
@@ -1198,8 +1198,7 @@ impl OpenCADStudio {
}
"DATAEXTRACTION" | "EATTEXT" | "ATTEXT" => {
let csv = build_data_extraction_csv(&self.tabs[i].scene.document);
return Some(Task::done(Message::DataExtractionSave(csv)));
self.open_data_extraction();
}
// ── Find / Replace ────────────────────────────────────────────────
@@ -1935,82 +1934,6 @@ fn z_axis_normal(normal: acadrust::types::Vector3) -> bool {
normal.x == 0.0 && normal.y == 0.0 && normal.z != 0.0
}
// ── 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 = crate::entities::names::dxf_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.to_degrees(),
e.end_angle.to_degrees()
),
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) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Polyline(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Polyline2D(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Polyline3D(e) => crate::tf!("{} vertices", e.vertices.len()).into_owned(),
EntityType::Hatch(e) => format!("PAT={}", e.pattern.name),
EntityType::Dimension(e) => format!("{:.3}", e.base().actual_measurement),
EntityType::Spline(e) => crate::tf!("{} ctrl pts", e.control_points.len()).into_owned(),
_ => 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()
}
}
// ── CAL — arithmetic expression evaluator ──────────────────────────────────
// A small recursive-descent parser for `+ - * /`, parentheses, unary signs and
// decimal numbers. Self-contained (no external dependency).
+1 -1
View File
@@ -6,7 +6,7 @@ use std::path::PathBuf;
mod blocks;
mod dim;
mod display;
pub(crate) mod display;
mod draw;
mod fileops;
mod inquiry;
+43
View File
@@ -29,6 +29,7 @@ mod startup;
mod style_ops;
mod text_inline;
mod tolerance_dialog;
mod annotation_data;
mod update;
mod view;
mod visibility;
@@ -1092,6 +1093,12 @@ pub(super) struct OpenCADStudio {
/// without Apply, mirroring the style managers' staging.
scale_stage: Option<crate::app::style_ops::ScaleStage>,
// ── Annotation table/data dialogs ────────────────────────────────────
table_insert: crate::ui::window::annotation_data::TableInsertState,
data_link_manager: crate::ui::window::annotation_data::DataLinkManagerState,
data_link_parent_table: bool,
data_extraction: crate::ui::window::annotation_data::DataExtractionState,
// ── Plot Style Panel ──────────────────────────────────────────────────
/// Selected ACI index in the panel (1-255).
plotstyle_panel_aci: u8,
@@ -1747,6 +1754,9 @@ pub enum ModalKind {
/// Add / remove the annotation scales a single selected object has a
/// per-object representation for.
AnnoObjectScale,
InsertTable,
DataLinkManager,
DataExtraction,
/// The scene is drawn by a software rasterizer, or not at all: what that
/// means and what usually fixes it. Queued once per verdict; the status
/// bar's ⚠ pill reopens it.
@@ -3412,6 +3422,35 @@ pub enum Message {
/// Background extraction/write completion.
WblockWriteFinished(String, std::path::PathBuf, Result<(), String>),
// ── DATAEXTRACTION ────────────────────────────────────────────────────
TableInsertStyle(String),
TableInsertField(crate::ui::window::annotation_data::TableInsertField),
TableInsertApply,
DataLinkManagerOpen,
DataLinkNew,
DataLinkSelect(acadrust::types::Handle),
DataLinkEdit,
DataLinkEditCancel,
DataLinkField(crate::ui::window::annotation_data::DataLinkField),
DataLinkBrowse,
DataLinkBrowseResult(Option<std::path::PathBuf>),
DataLinkSave,
DataLinkDelete,
DataLinkInsert,
DataLinkClose,
DataExtractionOpen,
DataExtractionField(crate::ui::window::annotation_data::DataExtractionField),
DataExtractionBack,
DataExtractionNext,
DataExtractionBrowseSettings,
DataExtractionBrowseSettingsResult(Option<std::path::PathBuf>),
DataExtractionAddDrawings,
DataExtractionAddDrawingsResult(Vec<std::path::PathBuf>),
DataExtractionAddFolder,
DataExtractionAddFolderResult(Option<std::path::PathBuf>),
DataExtractionClearSources,
DataExtractionBrowseOutput,
DataExtractionBrowseOutputResult(Option<std::path::PathBuf>),
DataExtractionFinish,
/// Save the pre-built CSV string to a file chosen by the user.
DataExtractionSave(String),
/// Path chosen (or None = cancelled).
@@ -3780,6 +3819,10 @@ impl OpenCADStudio {
anno_object_scale_target: None,
scale_rename_buf: String::new(),
scale_stage: None,
table_insert: Default::default(),
data_link_manager: Default::default(),
data_link_parent_table: false,
data_extraction: Default::default(),
layout_manager_rename_buf: String::new(),
plotstyle_panel_aci: 1,
ps_color_buf: String::new(),
+53
View File
@@ -1070,6 +1070,59 @@ impl OpenCADStudio {
Task::none()
}
Message::TableInsertStyle(value) => self.on_table_insert_style(value),
Message::TableInsertField(field) => self.on_table_insert_field(field),
Message::TableInsertApply => self.on_table_insert_apply(),
Message::DataLinkManagerOpen => {
let from_table = self.active_modal == Some(super::ModalKind::InsertTable);
self.open_data_link_manager(from_table);
Task::none()
}
Message::DataLinkNew => self.on_data_link_new(),
Message::DataLinkSelect(handle) => self.on_data_link_select(handle),
Message::DataLinkEdit => self.on_data_link_edit(),
Message::DataLinkEditCancel => {
self.data_link_manager.editing = false;
self.data_link_manager.editing_handle = None;
self.data_link_manager.status.clear();
Task::none()
}
Message::DataLinkField(field) => self.on_data_link_field(field),
Message::DataLinkBrowse => self.on_data_link_browse(),
Message::DataLinkBrowseResult(path) => self.on_data_link_browse_result(path),
Message::DataLinkSave => self.on_data_link_save(),
Message::DataLinkDelete => self.on_data_link_delete(),
Message::DataLinkInsert => self.on_data_link_insert(),
Message::DataLinkClose => self.on_data_link_close(),
Message::DataExtractionOpen => {
self.open_data_extraction();
Task::none()
}
Message::DataExtractionField(field) => self.on_data_extraction_field(field),
Message::DataExtractionBack => self.on_data_extraction_back(),
Message::DataExtractionNext => self.on_data_extraction_next(),
Message::DataExtractionBrowseSettings => self.on_data_extraction_browse_settings(),
Message::DataExtractionBrowseSettingsResult(path) => {
self.on_data_extraction_browse_settings_result(path)
}
Message::DataExtractionAddDrawings => self.on_data_extraction_add_drawings(),
Message::DataExtractionAddDrawingsResult(paths) => {
self.on_data_extraction_add_drawings_result(paths)
}
Message::DataExtractionAddFolder => self.on_data_extraction_add_folder(),
Message::DataExtractionAddFolderResult(path) => {
self.on_data_extraction_add_folder_result(path)
}
Message::DataExtractionClearSources => {
self.data_extraction.source_files.clear();
Task::none()
}
Message::DataExtractionBrowseOutput => self.on_data_extraction_browse_output(),
Message::DataExtractionBrowseOutputResult(path) => {
self.on_data_extraction_browse_output_result(path)
}
Message::DataExtractionFinish => self.on_data_extraction_finish(),
Message::DataExtractionSave(csv) => {
let csv_clone = csv.clone();
Task::perform(
+21
View File
@@ -31,6 +31,9 @@ impl OpenCADStudio {
Some(K::LayoutManager) => crate::tr!("modal", "layout-manager"),
Some(K::ScaleManager) => crate::tr!("modal", "scale-manager"),
Some(K::AnnoObjectScale) => crate::tr!("modal", "annotation-object-scale"),
Some(K::InsertTable) => crate::t!("Insert Table").into_owned(),
Some(K::DataLinkManager) => crate::t!("Data Link Manager").into_owned(),
Some(K::DataExtraction) => crate::t!("Data Extraction Wizard").into_owned(),
Some(K::Plotstyle) => crate::tr!("modal", "plot-style-editor"),
Some(K::TextStyle) => crate::tr!("modal", "text-style-manager"),
Some(K::MlStyle) => crate::tr!("modal", "multiline-style-manager"),
@@ -581,6 +584,24 @@ impl OpenCADStudio {
},
)
}
super::super::ModalKind::InsertTable => sized_flow(
ex,
620,
650,
|flow| crate::ui::window::annotation_data::table_insert_view(&self.table_insert, flow),
),
super::super::ModalKind::DataLinkManager => sized_flow(
ex,
760,
560,
|flow| crate::ui::window::annotation_data::data_link_view(&self.data_link_manager, flow),
),
super::super::ModalKind::DataExtraction => sized_flow(
ex,
780,
570,
|flow| crate::ui::window::annotation_data::data_extraction_view(&self.data_extraction, flow),
),
super::super::ModalKind::Plotstyle => sized_flow(
ex,
780,