From 785db5683b9056b06f5368a088d5bd3005d87ed0 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 2 Oct 2026 17:22:08 +0300 Subject: [PATCH 1/2] feat(field): table formula buttons, block placeholders, attribute fields on insert, plot-time field stamping Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- Cargo.toml | 4 + locales/ar-SA/opencadstudio.ftl | 6 ++ locales/bg-BG/opencadstudio.ftl | 6 ++ locales/cs-CZ/opencadstudio.ftl | 6 ++ locales/de-DE/opencadstudio.ftl | 6 ++ locales/el-GR/opencadstudio.ftl | 6 ++ locales/en-US/opencadstudio.ftl | 6 ++ locales/es-ES/opencadstudio.ftl | 6 ++ locales/fi-FI/opencadstudio.ftl | 6 ++ locales/fr-FR/opencadstudio.ftl | 6 ++ locales/hi-IN/opencadstudio.ftl | 6 ++ locales/hu-HU/opencadstudio.ftl | 6 ++ locales/it-IT/opencadstudio.ftl | 6 ++ locales/ja-JP/opencadstudio.ftl | 6 ++ locales/ko-KR/opencadstudio.ftl | 6 ++ locales/nl-NL/opencadstudio.ftl | 6 ++ locales/pl-PL/opencadstudio.ftl | 6 ++ locales/pt-BR/opencadstudio.ftl | 6 ++ locales/ru-RU/opencadstudio.ftl | 6 ++ locales/tr-TR/opencadstudio.ftl | 6 ++ locales/zh-CN/opencadstudio.ftl | 6 ++ locales/zh-TW/opencadstudio.ftl | 6 ++ src/app/command_driver/entity_commit.rs | 14 ++- src/app/commands/field.rs | 120 +++++++++++++++++++++++- src/app/control/actions.rs | 8 ++ src/app/update/file.rs | 16 ++++ src/entities/field.rs | 56 +++++++++++ src/locale_catalog.rs | 6 ++ src/modules/annotate/field_cmd.rs | 67 +++++++++++++ src/ui/window/field_dialog.rs | 115 +++++++++++++++++++++-- 31 files changed, 522 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51294fb85..ca1dd70d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4206,7 +4206,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.5.5" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=d96e3fa#d96e3fa2fe5acbeac966f1db4c01142618bf9c79" +source = "git+https://github.com/ramox81/cadcodec.git?rev=f94ea46#f94ea46e200360bf5dbe0680bc8090c45205db72" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 85cc56ce7..95a2d943b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,10 @@ objc2-app-kit = { version = "0.2", features = ["NSApplication", "NSResponder", " block2 = "0.5" objc2-foundation = { version = "0.2", features = ["NSArray", "NSURL", "NSString", "NSObject", "NSNotification", "NSThread", "NSEnumerator"] } +# Table formulas, block placeholders, plot-time fields and unit formats (opencadcodec #81). +[patch."https://github.com/HakanSeven12/opencadcodec.git"] +opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "f94ea46" } + [patch.crates-io] iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 97fec3890..b16eb479b 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -4105,6 +4105,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 0fb1d43e9..395758ea7 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -4092,6 +4092,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index d17ecfb1d..724bf6e48 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -4090,6 +4090,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 7fd9f0bd4..e5a685229 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -4091,6 +4091,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 53601f4be..aaa837e5b 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -4093,6 +4093,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index dcbbb7b69..b1e22ed13 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -4094,6 +4094,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index df1b19661..8c7b1d012 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -4093,6 +4093,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 99f18d514..4844bc1dd 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -4092,6 +4092,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index bc00d9334..b1ab5b3bf 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -4093,6 +4093,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 59440e35e..e0abed923 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -4081,6 +4081,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 2066c5156..3a412ad09 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -4092,6 +4092,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index bab01a81c..29901bfdc 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -4092,6 +4092,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index f0df48710..616fbee9e 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -4081,6 +4081,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 6ad109cc7..4e963349d 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -4092,6 +4092,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 2f5e39e39..80e8ac9eb 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -4091,6 +4091,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index c31421220..9fdaea28d 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -4092,6 +4092,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index e96dea04d..8901c55da 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -4093,6 +4093,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index e3eb50fa6..40697720e 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -4094,6 +4094,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 3b01d7c2c..5ca8fb648 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -4053,6 +4053,12 @@ insert = .field-select-object = Nesne seç .field-expression = Alan ifadesi .field-help = Bir alan ekler: çizimden, tarihten veya başka bir kaynaktan güncellenen metin. + .field-average = Ortalama + .field-sum = Toplam + .field-object-name = Nesne adı + .field-block-placeholder = Blok yer tutucusu + .field-block-editor-only = Yalnız blok düzenleyicide kullanılabilir + .field-block-reference-property = Blok başvurusu özelliği .field-cat-linked = Bağlantılı .field-current-precision = Geçerli hassasiyet .field-evaluate = Hesapla diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 290f64be0..078806de7 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -4076,6 +4076,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index c38cc4a04..3af162dc8 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -4088,6 +4088,12 @@ insert = .field-select-object = Select object .field-expression = Field expression .field-help = Inserts a field: text that updates from the drawing, the date or another source. + .field-average = Average + .field-sum = Sum + .field-object-name = Object name + .field-block-placeholder = Block placeholder + .field-block-editor-only = Only accessible in the block editor + .field-block-reference-property = Block reference property .field-cat-linked = Linked .field-current-precision = Current precision .field-evaluate = Evaluate diff --git a/src/app/command_driver/entity_commit.rs b/src/app/command_driver/entity_commit.rs index b2802535d..08c948bd6 100644 --- a/src/app/command_driver/entity_commit.rs +++ b/src/app/command_driver/entity_commit.rs @@ -231,7 +231,15 @@ impl OpenCADStudio { // objects are not part of the add delta, so the step is a snapshot. let text_field = self.tabs[i].active_cmd.as_ref().and_then(|cmd| cmd.text_field()); let label = self.history_label_from_active_cmd(i, "ENTITY"); - let delta_safe = text_field.is_none() && self.delta_add_safe(i, &entity); + // A block reference whose attribute definitions carry fields gets + // them copied onto its attributes (field objects: snapshot step). + let attribute_fields = match &entity { + codec::EntityType::Insert(insert) if !insert.attributes.is_empty() => { + crate::entities::field::block_has_attribute_fields(&self.tabs[i].scene.document, &insert.block_name) + } + _ => false, + }; + let delta_safe = text_field.is_none() && !attribute_fields && self.delta_add_safe(i, &entity); let field_value = match &entity { codec::EntityType::MText(m) => m.value.clone(), codec::EntityType::AttributeDefinition(a) => a.default_value.clone(), @@ -264,6 +272,10 @@ impl OpenCADStudio { &field_value, ); } + if let (Some(handle), true) = (committed, attribute_fields) { + crate::entities::field::attach_attribute_fields(&mut self.tabs[i].scene.document, handle); + self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); + } if let Some(handle) = committed { self.apply_continuous_constraints(i, &[handle]); } diff --git a/src/app/commands/field.rs b/src/app/commands/field.rs index 5c084fc78..422e51bcb 100644 --- a/src/app/commands/field.rs +++ b/src/app/commands/field.rs @@ -3,7 +3,7 @@ use crate::app::{Message, OpenCADStudio}; use crate::command::CadCommand; -use crate::modules::annotate::field_cmd::{FieldObjectPickCommand, FieldPlaceCommand}; +use crate::modules::annotate::field_cmd::{FieldObjectPickCommand, FieldPlaceCommand, FieldTablePickCommand}; use crate::ui::window::field_dialog::{ fields_of, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget, DATE_FORMATS, NAMED_TYPES, @@ -35,13 +35,70 @@ impl OpenCADStudio { } Some(Task::none()) } + cmd if cmd == "_FIELD_CELL" || cmd.starts_with("_FIELD_CELL ") => { + let parts: Vec<&str> = cmd.split_whitespace().skip(1).collect(); + if let Some(expression) = self.table_cell_expression(i, &parts) { + if let Some(state) = self.field_dialog.as_mut() { + // The reference appends at the end, with no operator. + state.formula.push_str(&expression); + } + self.refresh_field_preview(); + // A real result gets the Decimal format, as the reference picks it. + if let Some(state) = self.field_dialog.as_mut() { + if state.formula_format == 0 && state.preview.contains('.') { + state.formula_format = 2; + } + } + } + if self.field_dialog.is_some() { + self.active_modal = Some(crate::app::ModalKind::Field); + self.refresh_field_preview(); + } + Some(Task::none()) + } _ => None, } } + /// The formula text for a picked table cell or cell range: + /// `Table(%<\_ObjId H>%).B4` or `Table(%<\_ObjId H>%).Evaluate(Sum(A3:B5))`. + fn table_cell_expression(&self, i: usize, parts: &[&str]) -> Option { + let function = *parts.first()?; + let cell = |at: &[&str]| -> Option<(codec::Handle, usize, usize)> { + let handle = codec::Handle::new(u64::from_str_radix(at.first()?, 16).ok()?); + let point = glam::DVec3::new(at.get(1)?.parse().ok()?, at.get(2)?.parse().ok()?, at.get(3)?.parse().ok()?); + let document = &self.tabs[i].scene.document; + let codec::EntityType::Table(table) = document.get_entity(handle)? else { + return None; + }; + let hit = crate::modules::annotate::table_cmd::table_cell_at(table, None, point)?; + Some((handle, hit.row, hit.column)) + }; + let name = |row: usize, column: usize| format!("{}{}", column_letters(column), row + 1); + let (table, row, column) = cell(parts.get(1..5)?)?; + if function == "Cell" { + return Some(format!("Table(%<\\_ObjId {:X}>%).{}", table.value(), name(row, column))); + } + let (second_table, row2, column2) = cell(parts.get(5..9)?)?; + if second_table != table { + return None; + } + Some(format!( + "Table(%<\\_ObjId {:X}>%).Evaluate({function}({}:{}))", + table.value(), + name(row.min(row2), column.min(column2)), + name(row.max(row2), column.max(column2)), + )) + } + /// Open the Field dialog for `target`. pub(in crate::app) fn open_field_dialog(&mut self, target: FieldTarget) { let mut state = FieldDialogState::new(target); + if !matches!(target, FieldTarget::NewText) { + state.placeholder_block = self.tabs[self.active_tab] + .active_block_edit_session() + .map(|session| session.block_name.clone()); + } state.examples = DATE_FORMATS .iter() .map(|f| self.field_value(&format!("\\AcVar Date \\f \"{f}\""), &[])) @@ -62,7 +119,17 @@ impl OpenCADStudio { return; }; let (code, objects) = state.code(); - let preview = if state.complete() { self.field_value(&code, &objects) } else { String::new() }; + let preview = if state.complete() { + if state.name == "BlockPlaceholder" { + // A placeholder shows its property name until a block reference resolves it. + let (label, _, _) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[state.placeholder_property]; + text_case(label, state.text_case) + } else { + self.field_value(&code, &objects) + } + } else { + String::new() + }; if let Some(state) = self.field_dialog.as_mut() { state.preview = preview; } @@ -155,6 +222,18 @@ impl OpenCADStudio { ); } FieldDialogMsg::PlotScale(k) => state.plot_scale = k, + FieldDialogMsg::PlaceholderProperty(k) => { + state.placeholder_property = k; + state.text_case = 0; + } + FieldDialogMsg::TableFunction(function) => { + // The dialog waits while the cells are picked. + self.active_modal = None; + let command = FieldTablePickCommand::new(function); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + return Task::none(); + } FieldDialogMsg::SelectObject => { // The dialog waits while one object is picked. self.active_modal = None; @@ -226,3 +305,40 @@ impl OpenCADStudio { }; } } + +/// Spreadsheet column letters: A … Z, AA, AB … +fn column_letters(mut column: usize) -> String { + let mut letters = Vec::new(); + loop { + letters.push(b'A' + (column % 26) as u8); + if column < 26 { + break; + } + column = column / 26 - 1; + } + letters.reverse(); + String::from_utf8(letters).unwrap_or_default() +} + +/// The Field dialog's text cases: (none), Uppercase, Lowercase, First capital, Title case. +fn text_case(value: &str, case: usize) -> String { + match case { + 1 => value.to_uppercase(), + 2 => value.to_lowercase(), + 3 => { + let lower = value.to_lowercase(); + let mut chars = lower.chars(); + chars.next().map(|c| c.to_uppercase().chain(chars).collect()).unwrap_or_default() + } + 4 => value + .split(' ') + .map(|word| { + let lower = word.to_lowercase(); + let mut chars = lower.chars(); + chars.next().map(|c| c.to_uppercase().chain(chars).collect::()).unwrap_or_default() + }) + .collect::>() + .join(" "), + _ => value.to_string(), + } +} diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 671a08b77..329c6687a 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -362,6 +362,7 @@ impl OpenCADStudio { if !path.to_ascii_lowercase().ends_with(".pdf") { return Err(failure("invalid_path", "plot writes .pdf files")); } + self.stamp_plot_fields(); let i = self.active_tab; let names = self.tabs[i].scene.layout_names(); let requested = req["layout"].as_str().unwrap_or("Model").to_owned(); @@ -1019,6 +1020,13 @@ impl OpenCADStudio { "link_text" => F::HyperlinkText(v.into()), "link_url" => F::HyperlinkUrl(v.into()), "plot_scale" => F::PlotScale(index(codec::fields::PLOT_SCALE_FORMATS.len())?), + "placeholder" => F::PlaceholderProperty(index(codec::fields::BLOCK_PLACEHOLDER_PROPERTIES.len())?), + "table_function" => F::TableFunction( + ["Average", "Sum", "Count", "Cell"] + .into_iter() + .find(|f| f.eq_ignore_ascii_case(v)) + .ok_or_else(|| failure("invalid_value", "Average|Sum|Count|Cell"))?, + ), _ => return Err(failure("invalid_value", "Unknown field dialog key")), }) } diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 2edd18786..ca2945cc5 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -3764,7 +3764,20 @@ impl OpenCADStudio { task } + /// A plot gives its fields (PlotDate) the plot time, as the reference + /// stores it; the hosts are redrawn before the plot reads the scene. + pub(in crate::app) fn stamp_plot_fields(&mut self) { + let i = self.active_tab; + let hosts = crate::entities::field::stamp_plot_fields(&mut self.tabs[i].scene.document); + if !hosts.is_empty() { + let changes: Vec<_> = hosts.into_iter().map(|h| (h, crate::scene::ChangeKind::Modified)).collect(); + self.tabs[i].scene.bump_entities(&changes); + self.tabs[i].dirty = true; + } + } + fn print_all_pages(&mut self) -> Result, String> { + self.stamp_plot_fields(); let available = self.tabs[self.active_tab].scene.layout_names(); let selected: Vec = self .print_all_layouts @@ -5337,6 +5350,9 @@ impl OpenCADStudio { } self.active_modal = None; self.reset_modal_geometry(); + if !preview { + self.stamp_plot_fields(); + } // Extents, Window and Display use one plot path in both spaces. Only // Paper-space Layout is special: it uses the physical sheet bounds. diff --git a/src/entities/field.rs b/src/entities/field.rs index c05b5ccb3..fe63acc33 100644 --- a/src/entities/field.rs +++ b/src/entities/field.rs @@ -360,3 +360,59 @@ fn display_name() -> Option { #[cfg(not(target_os = "windows"))] None } + +/// Copy each attribute definition's field onto the matching attribute of a +/// new block reference (block placeholders resolve against the reference). +/// Returns the attributes that received a field. +pub fn attach_attribute_fields(document: &mut CadDocument, insert: Handle) -> Vec { + let context = OcsFieldContext(None); + document.attach_attribute_fields(insert, &context) +} + +/// Whether any attribute definition of `block` hosts a field. +pub fn block_has_attribute_fields(document: &CadDocument, block: &str) -> bool { + let Some(record) = document.block_records.iter().find(|r| r.name.eq_ignore_ascii_case(block)) else { + return false; + }; + record.entity_handles.iter().filter_map(|h| document.get_entity(*h)).any(|e| { + matches!(e, codec::entities::EntityType::AttributeDefinition(_)) && hosts_field(document, e) + }) +} + +/// Give plot-time fields (PlotDate) the plot's time; returns the hosts whose +/// text changed. +pub fn stamp_plot_fields(document: &mut CadDocument) -> Vec { + let snapshot = document.clone(); + let context = PlotContext(OcsFieldContext(Some(&snapshot))); + document.stamp_plot_fields(&context) +} + +/// The app context while a plot is produced. +struct PlotContext<'a>(OcsFieldContext<'a>); + +impl FieldContext for PlotContext<'_> { + fn now_julian(&self) -> f64 { + self.0.now_julian() + } + fn file_times(&self) -> Option<(f64, f64)> { + self.0.file_times() + } + fn plotting(&self) -> bool { + true + } + fn login(&self) -> Option { + self.0.login() + } + fn getvar(&self, name: &str) -> Option { + self.0.getvar(name) + } + fn file_size(&self) -> Option { + self.0.file_size() + } + fn date_locale(&self) -> codec::fields::DateLocale { + self.0.date_locale() + } + fn getenv(&self, name: &str) -> Option { + self.0.getenv(name) + } +} diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 4ec7f0109..ba91e7781 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -235,6 +235,12 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Select object" => Some(("insert", "field-select-object")), "Field expression" => Some(("insert", "field-expression")), "Inserts a field: text that updates from the drawing, the date or another source." => Some(("insert", "field-help")), + "Average" => Some(("insert", "field-average")), + "Sum" => Some(("insert", "field-sum")), + "Object name" => Some(("insert", "field-object-name")), + "Block placeholder" => Some(("insert", "field-block-placeholder")), + "Only accessible in the block editor" => Some(("insert", "field-block-editor-only")), + "Block reference property" => Some(("insert", "field-block-reference-property")), "Linked" => Some(("insert", "field-cat-linked")), "Current precision" => Some(("insert", "field-current-precision")), "Evaluate" => Some(("insert", "field-evaluate")), diff --git a/src/modules/annotate/field_cmd.rs b/src/modules/annotate/field_cmd.rs index 53e62dfa4..73cf7d02d 100644 --- a/src/modules/annotate/field_cmd.rs +++ b/src/modules/annotate/field_cmd.rs @@ -189,4 +189,71 @@ impl CadCommand for FieldObjectPickCommand { } } +/// The Field dialog's Average / Sum / Count / Cell buttons: a cell range (or +/// one cell) is picked in a table and handed back as +/// `_FIELD_CELL [
]`. +pub struct FieldTablePickCommand { + pub function: &'static str, + first: Option<(Handle, DVec3)>, +} + +impl FieldTablePickCommand { + pub fn new(function: &'static str) -> Self { + Self { function, first: None } + } +} + +impl CadCommand for FieldTablePickCommand { + fn name(&self) -> &'static str { + "FIELD" + } + + fn prompt(&self) -> String { + match (self.function, self.first) { + ("Cell", _) => "Select table cell:".into(), + (_, None) => "Select first corner of table cell range:".into(), + _ => "Select second corner of table cell range:".into(), + } + } + + fn needs_entity_pick(&self) -> bool { + true + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { + if handle.is_null() { + return CmdResult::NeedPoint; + } + let at = |h: Handle, p: DVec3| format!("{:X} {} {} {}", h.value(), p.x, p.y, p.z); + match (self.function, self.first) { + ("Cell", _) => CmdResult::Dispatch(format!("_FIELD_CELL Cell {}", at(handle, pt))), + (_, None) => { + self.first = Some((handle, pt)); + CmdResult::NeedPoint + } + (function, Some((first, first_pt))) => CmdResult::Dispatch(format!( + "_FIELD_CELL {function} {} {}", + at(first, first_pt), + at(handle, pt) + )), + } + } + + fn on_point(&mut self, _pt: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Dispatch("_FIELD_CELL".into()) + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Dispatch("_FIELD_CELL".into()) + } +} + inventory::submit!(crate::command::CommandRegistration { names: &["FIELD"] }); diff --git a/src/ui/window/field_dialog.rs b/src/ui/window/field_dialog.rs index 0d76b6816..6943ba071 100644 --- a/src/ui/window/field_dialog.rs +++ b/src/ui/window/field_dialog.rs @@ -32,7 +32,7 @@ pub const CATEGORIES: &[(&str, &[&str])] = &[ &["Author", "Comments", "Filename", "Filesize", "HyperlinkBase", "Keywords", "LastSavedBy", "Subject", "Title"], ), ("Linked", &["Hyperlink"]), - ("Objects", &["Formula", "NamedObject", "Object"]), + ("Objects", &["BlockPlaceholder", "Formula", "NamedObject", "Object"]), ("Other", &["DieselExpression", "SystemVariable"]), ( "Plot", @@ -77,6 +77,7 @@ pub enum FieldKind { Formula, Hyperlink, PlotScale, + BlockPlaceholder, } /// Formula precisions: the current one, then 0 to 8 decimals. @@ -95,6 +96,7 @@ pub fn kind_of(name: &str) -> FieldKind { "Formula" => FieldKind::Formula, "Hyperlink" => FieldKind::Hyperlink, "PlotScale" => FieldKind::PlotScale, + "BlockPlaceholder" => FieldKind::BlockPlaceholder, _ => FieldKind::Text, } } @@ -128,6 +130,11 @@ pub struct FieldDialogState { pub hyperlink_url: String, /// Index into codec PLOT_SCALE_FORMATS. pub plot_scale: usize, + /// The block being edited in the block editor; block placeholders are + /// only offered there. + pub placeholder_block: Option, + /// Index into codec BLOCK_PLACEHOLDER_PROPERTIES. + pub placeholder_property: usize, pub preview: String, /// Today in each of DATE_FORMATS, for the Examples list. pub examples: Vec, @@ -158,6 +165,8 @@ impl FieldDialogState { hyperlink_text: String::new(), hyperlink_url: String::new(), plot_scale: 0, + placeholder_block: None, + placeholder_property: 7, preview: String::new(), examples: Vec::new(), } @@ -195,10 +204,11 @@ impl FieldDialogState { 0 => String::new(), n => format!("%pr{}", n - 1), }; - let body = format!("\\AcExpr ({})", self.formula.trim()); + let (formula, objects) = indexed_object_ids(self.formula.trim()); + let body = format!("\\AcExpr ({formula})"); match format!("{format}{precision}") { - f if f.is_empty() => (body, vec![]), - f => (format!("{body} \\f \"{f}\""), vec![]), + f if f.is_empty() => (body, objects), + f => (format!("{body} \\f \"{f}\""), objects), } } FieldKind::Hyperlink => ( @@ -211,6 +221,21 @@ impl FieldDialogState { .map_or_else(|| "\\AcVar PlotScale".to_string(), |f| f.1.to_string()), vec![], ), + FieldKind::BlockPlaceholder => { + let (_, property, format) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[self.placeholder_property]; + // Text properties take the chosen case; the others keep their own format. + let format = match (format, self.text_case) { + ("%tc4", 0) => String::new(), + ("%tc4", n) => format!("%tc{n}"), + (other, _) => other.to_string(), + }; + let code = format!("\\AcObjProp.16.2 Object(?BlockRefId,1).{property}"); + if format.is_empty() { + (code, vec![]) + } else { + (format!("{code} \\f \"{format}\""), vec![]) + } + } FieldKind::SystemVariable => (format!("\\AcVar {}", self.sysvar), vec![]), FieldKind::Diesel => (format!("\\AcDiesel {}", self.diesel), vec![]), FieldKind::NamedObject => match self.named.and_then(|i| self.named_names.get(i)) { @@ -235,11 +260,35 @@ impl FieldDialogState { FieldKind::Diesel => !self.diesel.trim().is_empty(), FieldKind::Formula => !self.formula.trim().is_empty(), FieldKind::Hyperlink => !self.hyperlink_url.trim().is_empty(), + FieldKind::BlockPlaceholder => self.placeholder_block.is_some(), _ => true, } } } +/// Table references in a formula are typed as `%<\_ObjId HANDLE>%`; the stored +/// code numbers them (`%<\_ObjIdx n>%`) and lists the objects. +fn indexed_object_ids(formula: &str) -> (String, Vec) { + let mut out = String::new(); + let mut objects: Vec = Vec::new(); + let mut rest = formula; + while let Some(start) = rest.find("%<\\_ObjId ") { + let after = &rest[start + "%<\\_ObjId ".len()..]; + let Some(end) = after.find(">%") else { break }; + let Ok(value) = u64::from_str_radix(after[..end].trim(), 16) else { break }; + let handle = codec::Handle::new(value); + let index = objects.iter().position(|h| *h == handle).unwrap_or_else(|| { + objects.push(handle); + objects.len() - 1 + }); + out.push_str(&rest[..start]); + out.push_str(&format!("%<\\_ObjIdx {index}>%")); + rest = &after[end + 2..]; + } + out.push_str(rest); + (out, objects) +} + /// The geometric properties a field can show for an object of this type. pub fn object_properties(entity: &codec::EntityType) -> Vec<&'static str> { use codec::EntityType as E; @@ -274,6 +323,9 @@ pub enum FieldDialogMsg { FormulaFormat(usize), FormulaPrecision(usize), Evaluate, + /// Sum / Average / Count of a cell range, or one Cell, picked in a table. + TableFunction(&'static str), + PlaceholderProperty(usize), HyperlinkText(String), HyperlinkUrl(String), BrowseHyperlink, @@ -334,6 +386,14 @@ impl IntoContainer for text_input::Style { } } +fn table_button<'a>(label: String, function: &'static str) -> Element<'a, Message> { + button(text(label).size(12)) + .on_press(msg(FieldDialogMsg::TableFunction(function))) + .style(button_style(false)) + .padding([5, 10]) + .into() +} + fn case_list<'a>(state: &FieldDialogState) -> Element<'a, Message> { list( TEXT_CASES @@ -529,6 +589,36 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> { ] .spacing(8) .into(), + FieldKind::BlockPlaceholder => match &state.placeholder_block { + None => column![text(t!("Only accessible in the block editor")).size(12).style(muted_style)].into(), + Some(block) => { + let (_, _, format) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[state.placeholder_property]; + let mut panel = column![ + row![ + text(t!("Block name")).size(12).width(Length::Fixed(110.0)), + container(text(block.clone()).size(12).style(muted_style)).padding([5, 8]).width(Fill), + ] + .spacing(8) + .align_y(iced::Center), + text(t!("Block reference property")).size(12), + list( + codec::fields::BLOCK_PLACEHOLDER_PROPERTIES + .iter() + .enumerate() + .map(|(i, (label, _, _))| { + (t!(*label).into_owned(), state.placeholder_property == i, FieldDialogMsg::PlaceholderProperty(i)) + }) + .collect(), + 150.0, + ), + ] + .spacing(8); + if format == "%tc4" { + panel = panel.push(text(t!("Format")).size(12)).push(case_list(state)); + } + panel.into() + } + }, FieldKind::PlotScale => column![ text(t!("Format")).size(12), list( @@ -617,10 +707,18 @@ pub fn view<'a>( .font(iced::Font::MONOSPACE) .style(field_style) .on_input(|v| msg(FieldDialogMsg::Formula(v))), - button(text(t!("Evaluate")).size(12)) - .on_press(msg(FieldDialogMsg::Evaluate)) - .style(button_style(false)) - .padding([5, 14]), + row![ + table_button(t!("Average").into_owned(), "Average"), + table_button(t!("Sum").into_owned(), "Sum"), + table_button(t!("Count").into_owned(), "Count"), + table_button(t!("Cell").into_owned(), "Cell"), + Space::new().width(Fill), + button(text(t!("Evaluate")).size(12)) + .on_press(msg(FieldDialogMsg::Evaluate)) + .style(button_style(false)) + .padding([5, 14]), + ] + .spacing(6), ] .spacing(8), ), @@ -629,6 +727,7 @@ pub fn view<'a>( ] .spacing(10), FieldKind::Hyperlink => column![card(t!("Hyperlink").into_owned(), format_panel(state)), preview].spacing(10), + FieldKind::BlockPlaceholder => column![card(t!("Block placeholder").into_owned(), format_panel(state)), preview].spacing(10), _ => column![card(t!("Format").into_owned(), format_panel(state)), preview].spacing(10), }; let (code, _) = state.code(); From 3ae291255852f81eb05888c60ac3a66d890490b6 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:07:31 +0300 Subject: [PATCH 2/2] fix(insert): ATTREQ 0 inserts attributes with their defaults, without prompting Co-Authored-By: Claude Opus 5.5 --- src/app/command_driver/entity_commit.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/command_driver/entity_commit.rs b/src/app/command_driver/entity_commit.rs index 08c948bd6..4656fab0c 100644 --- a/src/app/command_driver/entity_commit.rs +++ b/src/app/command_driver/entity_commit.rs @@ -984,7 +984,7 @@ impl OpenCADStudio { let i = self.active_tab; // Collect the full AttributeDefinitions owned by this block // record so each created attribute keeps its geometry (#255). - let attdefs: Vec = { + let mut attdefs: Vec = { let doc = &self.tabs[i].scene.document; if let Some(br) = doc.block_records.get(&block_name) { br.entity_handles @@ -1004,6 +1004,12 @@ impl OpenCADStudio { } }; + // ATTREQ 0: attributes take their defaults without prompting. + if !self.tabs[i].scene.document.header.attribute_request { + for attdef in &mut attdefs { + attdef.flags.preset = true; + } + } if attdefs.is_empty() { // No attribute definitions — commit the INSERT directly. let entity = self.tabs[i]