diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 5e157bc89..a0937a7d3 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -6639,3 +6639,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 57dbafb7d..819268a20 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -6627,3 +6627,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 72c2589f3..b5e2fc9ae 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -6624,3 +6624,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index b63ac374d..4f5cb9d78 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -6625,3 +6625,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 06f421ebd..6961fbf27 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -6627,3 +6627,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index b47f75bdb..c369cf6a0 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -6626,3 +6626,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index ea684eeeb..cca581ec6 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -6627,3 +6627,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 8e1f53068..e68d7098c 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -6624,3 +6624,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 647d9f5dd..b4f4407ae 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -6627,3 +6627,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 0396093e6..861ab38c9 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -6615,3 +6615,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 27b5a5f84..862f462b9 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -6611,3 +6611,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index abae73884..12e98b3e8 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -6626,3 +6626,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index b2acabde6..aedd85f25 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -6615,3 +6615,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 81b0d0ec7..ffb94b155 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -6626,3 +6626,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index f65123729..3d19a96bf 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -6627,3 +6627,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 1c7e40b21..05869917e 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -6625,3 +6625,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 387403277..f6fce5956 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -6626,3 +6626,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index e830afe38..fc3d411b9 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -6627,3 +6627,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index efbde2ff7..1005ba506 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -6628,3 +6628,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index de8e7e739..a97d3a28a 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -6548,3 +6548,20 @@ attman = .sync = Eşitle .help = BATTMAN bir bloğun öznitelik tanımlarını yönetir. .no-attributed-block = Blok tablosunda __ocs_fmt_0__ adlı öznitelikli blok yok. +txt2mtxt = + .title = Metni Birleştir — Ayarlar + .result = Sonuç + .combine = Tek bir MText nesnesinde birleştir + .order = Satır sırası + .top-down = Üstten aşağıya + .selection-order = Seçim sırası + .combine-note = Kapalıyken her metin ayrı MText olur; sıra seçici pasifleşir. + .lines = Satırlar + .wrap = Sözcük kaydırma + .wrap-hint = — satırlar boşlukla birleşir, genişlik hesaplanır + .uniform = Satır aralığını eşitle + .uniform-hint = (sözcük kaydırma açıkken her zaman eşit) + .selected = Seçilen + .result-count = Sonuç · { $count } MText + .apply = Uygula + .help = TXT2MTXT komutunun metin nesnelerini MText olarak nasıl birleştireceğini ayarlar. diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 672514645..81d711f40 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -6610,3 +6610,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 5c0b4dc12..5b506f4ae 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -6622,3 +6622,20 @@ attman = .sync = Sync .help = BATTMAN manages the attribute definitions of a block. .no-attributed-block = Block table contains no attributed block named __ocs_fmt_0__. +txt2mtxt = + .title = Combine Text — Settings + .result = Result + .combine = Combine into a single MText object + .order = Line order + .top-down = Top-down + .selection-order = Selection order + .combine-note = When off, each text becomes its own MText; the order choice is inactive. + .lines = Lines + .wrap = Word-wrap + .wrap-hint = — lines join with spaces, the width is computed + .uniform = Force uniform line spacing + .uniform-hint = (always uniform while word-wrap is on) + .selected = Selected + .result-count = Result · { $count } MText + .apply = Apply + .help = Sets how TXT2MTXT combines text objects into MText. diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index 5eed4f834..c68293aad 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -445,6 +445,7 @@ impl OpenCADStudio { } => { self.finish_qselect_sample(*state, tab_id, handle); } + CmdResult::OpenTxt2MtxtSettings => self.open_txt2mtxt_settings(), CmdResult::OpenAutoConstrainSettings => { self.handle_open_auto_constrain_settings(); } diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index ba4a89d96..2cc809487 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -1335,6 +1335,30 @@ impl OpenCADStudio { } } + // TXT2MTXT — TEXT / MTEXT combined into MTEXT. A preselection is + // used at once; otherwise the selection prompt with SEttings. + "TXT2MTXT" => { + if !self.tabs[i].scene.selected.is_empty() { + return Some(self.dispatch_command("_TXT2MTXTSEL")); + } + use crate::modules::draw::select::SelectObjectsCommand; + self.command_line + .push_output(crate::t!("Select text objects to combine ...").as_ref()); + let command = SelectObjectsCommand::with_settings( + "TXT2MTXT", + "_TXT2MTXTSEL", + "Select objects or [SEttings]:", + || crate::command::CmdResult::OpenTxt2MtxtSettings, + ); + self.command_line.push_info(&command.prompt()); + self.command_line.set_step_options(command.options()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + "_TXT2MTXTSEL" => { + self.apply_txt2mtxt(i); + return Some(self.finish_dispatch("TXT2MTXT")); + } + "OFFSET" => { use crate::modules::draw::modify::offset::{is_offsettable, OffsetCommand}; let all_entities: Vec<_> = self.tabs[i] @@ -1546,3 +1570,52 @@ fn title_case(s: &str) -> String { .collect::>() .join(" ") } + +impl OpenCADStudio { + /// TXT2MTXT on the selection (in selection order), as one undo step. + fn apply_txt2mtxt(&mut self, i: usize) { + let scene = &self.tabs[i].scene; + let handles: Vec = scene + .selected_handles_in_order() + .into_iter() + .filter(|handle| !scene.is_layer_locked(*handle)) + .collect(); + let plan = { + let selected: Vec<(codec::Handle, &codec::EntityType)> = handles + .iter() + .filter_map(|h| scene.document.get_entity(*h).map(|e| (*h, e))) + .collect(); + crate::modules::annotate::txt2mtxt::plan( + &selected, + &scene.document, + self.txt2mtxt_settings, + ) + }; + self.tabs[i].scene.deselect_all(); + // Enter on an empty selection ends quietly. + if handles.is_empty() { + return; + } + let Some(plan) = plan else { + self.command_line + .push_output(" 0 Text objects removed, 0 MText objects added."); + return; + }; + self.push_undo_snapshot(i, "TXT2MTXT"); + self.tabs[i].scene.erase_entities(&plan.erase); + let added = plan.add.len(); + for entity in plan.add { + self.tabs[i].scene.add_entity(entity); + } + self.tabs[i].dirty = true; + self.refresh_properties(); + // Every selected object counts as removed text, the way the + // reference reports it. + let removed = handles.len(); + self.command_line.push_output(&if added == 1 { + format!(" {removed} Text objects removed, 1 MText object added.") + } else { + format!(" {removed} Text objects removed, {added} MText objects added.") + }); + } +} diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 442eb1cec..8bb6df31e 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -779,6 +779,10 @@ inventory::submit!(crate::command::CommandRegistration { "UCSDETECT", "SURFACEASSOCIATIVITY", "LAYOUTCREATEVIEWPORT", + "TXT2MTXTCOMBINEMTEXT", + "TXT2MTXTTEXTORDER", + "TXT2MTXTWORDWRAP", + "TXT2MTXTLINESPACING", "FIELDEVAL", "PDFIMPORTMODE", "PDFIMPORTFILTER", diff --git a/src/app/commands/pdf_dialogs.rs b/src/app/commands/pdf_dialogs.rs index ebdcf0aa1..048c6828a 100644 --- a/src/app/commands/pdf_dialogs.rs +++ b/src/app/commands/pdf_dialogs.rs @@ -348,6 +348,12 @@ impl OpenCADStudio { self.active_modal = Some(crate::app::ModalKind::PdfImportSettings); } + /// TXT2MTXT's SEttings option: the dialog over the running command. + pub(in crate::app) fn open_txt2mtxt_settings(&mut self) { + self.txt2mtxt_dialog = Some(self.txt2mtxt_settings); + self.active_modal = Some(crate::app::ModalKind::Txt2MtxtSettings); + } + /// PDFIMPORT's File option after the file is chosen. pub(in crate::app) fn open_pdf_import_file(&mut self, path: &str) { self.pdf_import_file = Some(PdfImportFileState { @@ -442,6 +448,7 @@ impl OpenCADStudio { crate::t!("Sets the color schemes and ranges a point cloud is colored by intensity or elevation.") } "layers" => crate::t!("Turns the layers of a PDF underlay on or off."), + "txt2mtxt" => std::borrow::Cow::Owned(crate::tr!("txt2mtxt", "help")), _ => crate::t!("Imports the geometry, fills, raster images and text of a PDF file as drawing objects."), }; self.command_line.push_info(text.as_ref()); @@ -726,6 +733,37 @@ impl OpenCADStudio { self.close_active_modal(); } + // ── Text to MText Settings ──────────────────────────────────── + PdfDialogMsg::T2mCombine(on) => { + if let Some(s) = self.txt2mtxt_dialog.as_mut() { + s.combine = on; + } + } + PdfDialogMsg::T2mSelectOrder(on) => { + if let Some(s) = self.txt2mtxt_dialog.as_mut() { + s.select_order = on; + } + } + PdfDialogMsg::T2mWrap(on) => { + if let Some(s) = self.txt2mtxt_dialog.as_mut() { + s.word_wrap = on; + } + } + PdfDialogMsg::T2mUniform(on) => { + if let Some(s) = self.txt2mtxt_dialog.as_mut() { + s.uniform_spacing = on; + } + } + PdfDialogMsg::T2mOk => { + if let Some(s) = self.txt2mtxt_dialog { + if s != self.txt2mtxt_settings { + self.txt2mtxt_settings = s; + self.save_config(); + } + } + self.close_active_modal(); + } + // ── Import PDF ──────────────────────────────────────────────── PdfDialogMsg::ImportBrowse => return Task::done(Message::PdfImportPick), PdfDialogMsg::ImportPage(page) => { diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index f99c20694..f067593ed 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1282,6 +1282,10 @@ impl OpenCADStudio { | "DYNCONSTRAINTDISPLAY" | "CCONSTRAINTFORM" | "DYNMODE" + | "TXT2MTXTCOMBINEMTEXT" + | "TXT2MTXTTEXTORDER" + | "TXT2MTXTWORDWRAP" + | "TXT2MTXTLINESPACING" ) => { return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i); @@ -1578,6 +1582,50 @@ impl OpenCADStudio { } return Some(self.finish_dispatch(cmd)); } + if let Some(field) = match name.as_str() { + "TXT2MTXTCOMBINEMTEXT" => Some(0), + "TXT2MTXTTEXTORDER" => Some(1), + "TXT2MTXTWORDWRAP" => Some(2), + "TXT2MTXTLINESPACING" => Some(3), + _ => None, + } { + // TXT2MTXT's profile settings: 0 / 1 (TEXTORDER 1 = + // selection order). + let s = &mut self.txt2mtxt_settings; + let slot = match field { + 0 => &mut s.combine, + 1 => &mut s.select_order, + 2 => &mut s.word_wrap, + _ => &mut s.uniform_spacing, + }; + let current = i16::from(*slot); + match value + .as_deref() + .map(|v| v.trim().parse::().ok().filter(|v| (0..=1).contains(v))) + { + Some(Some(mode)) => { + if current != mode { + *slot = mode == 1; + self.save_config(); + } + } + Some(None) => { + self.command_line + .push_error(crate::t!("Requires 0 or 1 only.").as_ref()); + self.command_line.push_output(&format!( + "Enter new value for {name} <{current}>:" + )); + self.pending_setvar = Some(name.clone()); + } + None => { + self.command_line.push_output(&format!( + "Enter new value for {name} <{current}>:" + )); + self.pending_setvar = Some(name.clone()); + } + } + return Some(self.finish_dispatch(cmd)); + } if name == "FIELDDISPLAY" { let current = i16::from(crate::entities::field::display()); match value diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 915337d97..d51fcdb4e 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -105,6 +105,7 @@ pub(super) const NAMES: &[&str] = &[ #[cfg(not(target_arch = "wasm32"))] "external_change_cancel", "pdf_dialog_ok", + "txt2mtxt_settings", "view_background", "image_adjust", "refedit_dialog", @@ -2202,6 +2203,27 @@ impl OpenCADStudio { _ => return Err(bad()), } } + // Text to MText Settings (TXT2MTXT SEttings): "combine=0", + // "order=select" / "order=top", "wrap=0", "uniform=1", "ok", + // "cancel". + "txt2mtxt_settings" => { + use crate::ui::window::pdf_dialogs::PdfDialogMsg as M; + if self.active_modal != Some(crate::app::ModalKind::Txt2MtxtSettings) { + return Err(failure("no_dialog", "Text to MText Settings is not open")); + } + let value = string(req, "value")?; + let (key, arg) = value.split_once('=').unwrap_or((value, "")); + let on = arg != "0"; + match key { + "combine" => Message::PdfDialog(M::T2mCombine(on)), + "order" => Message::PdfDialog(M::T2mSelectOrder(arg == "select")), + "wrap" => Message::PdfDialog(M::T2mWrap(on)), + "uniform" => Message::PdfDialog(M::T2mUniform(on)), + "ok" => Message::PdfDialog(M::T2mOk), + "cancel" => Message::CloseModal, + _ => return Err(failure("invalid_value", "Unknown setting")), + } + } "pdf_dialog_ok" => { use crate::ui::window::pdf_dialogs::PdfDialogMsg; Message::PdfDialog(match self.active_modal { diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index 079abef39..cd1dccfd1 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -381,6 +381,7 @@ fn command_selection_policy(name: &str) -> &'static str { "ERASE", "EXPLODE", "TXTEXP", + "TXT2MTXT", "GROUP", "LAYFRZ", "LAYLCK", diff --git a/src/app/mod.rs b/src/app/mod.rs index 72f4ee4e7..d69927ce3 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -787,6 +787,10 @@ pub(super) struct OpenCADStudio { /// pill shows only the glyph. pub show_constraint_values: bool, pub auto_constrain_settings: settings::AutoConstrainSettings, + /// TXT2MTXT settings (profile). + pub txt2mtxt_settings: settings::Txt2MtxtSettings, + /// The Text to MText Settings dialog's working copy. + txt2mtxt_dialog: Option, auto_constrain_saved: Option, auto_constrain_selected_row: usize, auto_constrain_distance_input: String, @@ -2205,6 +2209,8 @@ pub enum ModalKind { RefEdit, UnderlayLayers, PdfImportSettings, + /// TXT2MTXT's Text to MText Settings, over the running command. + Txt2MtxtSettings, PdfImportFile, XrefAttach, WriteBlock, @@ -4738,6 +4744,8 @@ impl OpenCADStudio { file_assoc_enabled: true, show_constraint_values: true, auto_constrain_settings: settings::AutoConstrainSettings::default(), + txt2mtxt_settings: settings::Txt2MtxtSettings::default(), + txt2mtxt_dialog: None, auto_constrain_saved: None, auto_constrain_selected_row: 0, auto_constrain_distance_input: "0.05".to_string(), diff --git a/src/app/settings.rs b/src/app/settings.rs index 2ffde4850..f2fc6bfea 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -108,6 +108,34 @@ impl AutoConstrainSettings { } } +/// TXT2MTXT (Combine Text) settings, the profile values +/// TXT2MTXTCOMBINEMTEXT, TXT2MTXTTEXTORDER, TXT2MTXTWORDWRAP and +/// TXT2MTXTLINESPACING. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct Txt2MtxtSettings { + /// Combine everything into one MTEXT (else each TEXT becomes its own). + pub combine: bool, + /// Keep the selection order (else sort top-down). + pub select_order: bool, + /// Run the lines on separated by spaces in a computed width (else one + /// paragraph per object). + pub word_wrap: bool, + /// Force uniform line spacing (always on while word-wrap is). + pub uniform_spacing: bool, +} + +impl Default for Txt2MtxtSettings { + fn default() -> Self { + Self { + combine: true, + select_order: false, + word_wrap: true, + uniform_spacing: true, + } + } +} + /// Cursor shown over the drawing viewport. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum CursorType { @@ -599,6 +627,9 @@ pub struct UserSettings { /// the Auto Constrain command. #[serde(default)] pub auto_constrain: AutoConstrainSettings, + /// TXT2MTXT settings (Text to MText Settings dialog). + #[serde(default)] + pub txt2mtxt: Txt2MtxtSettings, /// Keep existing geometry size while solving after a constraint edit. #[serde(default = "default_constraint_solve_mode")] pub constraint_solve_mode: bool, @@ -995,6 +1026,7 @@ impl Default for UserSettings { file_assoc_enabled: true, show_constraint_values: true, auto_constrain: AutoConstrainSettings::default(), + txt2mtxt: Txt2MtxtSettings::default(), constraint_solve_mode: true, constraint_infer: false, constraint_bar_display: 3, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 3062d905f..bd2024d62 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -900,6 +900,7 @@ impl OpenCADStudio { file_assoc_enabled: self.file_assoc_enabled, show_constraint_values: self.show_constraint_values, auto_constrain: self.auto_constrain_settings.clone(), + txt2mtxt: self.txt2mtxt_settings, constraint_solve_mode: self.constraint_solve_mode, constraint_infer: self.constraint_infer, constraint_bar_display: self.constraint_bar_display, @@ -1071,6 +1072,7 @@ impl OpenCADStudio { self.file_assoc_enabled = s.file_assoc_enabled; self.show_constraint_values = s.show_constraint_values; self.auto_constrain_settings = s.auto_constrain.clone(); + self.txt2mtxt_settings = s.txt2mtxt; self.auto_constrain_settings.sanitize(); self.auto_constrain_distance_input = format!("{}", self.auto_constrain_settings.distance_tolerance); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index ba32fe3de..26893dec2 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -239,6 +239,17 @@ impl OpenCADStudio { Some(PdfAttach) => self.pdf_attach = None, Some(UnderlayLayers) => self.underlay_layers = None, Some(PdfImportSettings) => self.pdf_import_settings = None, + // OK or Cancel: TXT2MTXT asks for its selection again. + Some(Txt2MtxtSettings) => { + self.txt2mtxt_dialog = None; + if let Some(prompt) = self.tabs[self.active_tab] + .active_cmd + .as_ref() + .map(|command| command.prompt()) + { + self.command_line.push_info(&prompt); + } + } Some(PdfImportFile) => self.pdf_import_file = None, Some(XrefAttach) => self.xref_attach = None, Some(WriteBlock) => self.wblock = None, diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index f696c42d6..49662d4d6 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -46,6 +46,7 @@ impl OpenCADStudio { Some(K::RefEdit) => crate::tr!("ref-edit", "dialog-title"), Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(), Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(), + Some(K::Txt2MtxtSettings) => crate::tr!("txt2mtxt", "title"), Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(), Some(K::XrefAttach) => crate::t!("Attach External Reference").into_owned(), Some(K::WriteBlock) => crate::t!("Write Block").into_owned(), @@ -577,6 +578,12 @@ impl OpenCADStudio { crate::ui::window::pdf_dialogs::view_import_settings(settings, flow) }) } + super::super::ModalKind::Txt2MtxtSettings => { + let settings = self.txt2mtxt_dialog.as_ref()?; + sized_flow(ex, 600, 470, |flow| { + crate::ui::window::pdf_dialogs::view_txt2mtxt_settings(settings, flow) + }) + } super::super::ModalKind::PdfImportFile => { let state = self.pdf_import_file.as_ref()?; sized_flow(ex, 960, 560, |flow| { diff --git a/src/command.rs b/src/command.rs index bca35fb77..9ce2550bc 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1812,6 +1812,9 @@ pub enum CmdResult { second: crate::scene::parametric_constraints::ParametricRef, label: &'static str, }, + /// Opens the Text to MText Settings dialog from TXT2MTXT's selection + /// prompt; the command keeps its prompt. + OpenTxt2MtxtSettings, /// Opens the Auto Constrain settings dialog from the selection prompt. OpenAutoConstrainSettings, /// Adds an ordered Coincident relation. Point/point selections create a diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index 54f91cc37..6b73ae6e3 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -114,6 +114,16 @@ fn display_text(t: &MText, document: &codec::CadDocument) -> String { /// click-to-select preview. Uses the exact same layout opts as `to_render` /// so the boxes line up with the rendered glyphs. pub fn glyph_boxes(t: &MText, document: &codec::CadDocument) -> Vec { + layout_of(t, document, true).glyph_boxes +} + +/// The MTEXT laid out as it is drawn: its sub-lines, their widths and the +/// first baseline, in the entity's own (pre-rotation) frame. +pub fn layout_of( + t: &MText, + document: &codec::CadDocument, + want_glyph_boxes: bool, +) -> crate::entities::text_support::MTextLayout { let resolved_style = resolve_text_style(&t.style, document); let attach_h_anchor: f32 = match t.attachment_point { AttachmentPoint::TopCenter @@ -143,7 +153,7 @@ pub fn glyph_boxes(t: &MText, document: &codec::CadDocument) -> Vec { // A field shows its stored value: fields update on their evaluation // events (open, save, plot, regen, UPDATEFIELD), never on a redraw. let display = display_text(t, document); - let layout = layout_mtext(&MTextRenderOpts { + layout_mtext(&MTextRenderOpts { value: &display, insertion: [ t.insertion_point.x, @@ -165,10 +175,9 @@ pub fn glyph_boxes(t: &MText, document: &codec::CadDocument) -> Vec { vertical_text: matches!(t.drawing_direction, DrawingDirection::TopToBottom) || matches!(t.drawing_direction, DrawingDirection::ByStyle) && resolved_style.is_vertical, - want_glyph_boxes: true, + want_glyph_boxes, columns: columns_of(t), - }); - layout.glyph_boxes + }) } fn to_render(t: &MText, document: &codec::CadDocument) -> RenderEntity { diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs index 5afa9ef02..db7fb38b4 100644 --- a/src/modules/annotate/mod.rs +++ b/src/modules/annotate/mod.rs @@ -29,6 +29,7 @@ pub mod radius_dim; pub mod table_cmd; pub mod text; pub mod textedit; +pub mod txt2mtxt; pub mod tolerance_cmd; use crate::modules::{CadModule, RibbonGroup, RibbonItem, StyleKey}; diff --git a/src/modules/annotate/txt2mtxt.rs b/src/modules/annotate/txt2mtxt.rs new file mode 100644 index 000000000..c0970a8aa --- /dev/null +++ b/src/modules/annotate/txt2mtxt.rs @@ -0,0 +1,361 @@ +//! TXT2MTXT (Combine Text): TEXT and MTEXT objects merged into one MTEXT, or +//! each TEXT (and each line of an MTEXT) turned into an MTEXT of its own. +//! +//! The combined MTEXT sits at the top-left corner of the first object (the +//! topmost one unless the selection order is kept) and takes its layer, style, +//! height and rotation. With word-wrap the objects run on separated by spaces +//! in a box as wide as their bounding box (plus half a percent), and an even +//! pitch between them becomes the line spacing factor. Without it each object +//! starts a paragraph; unless line spacing is forced uniform, each paragraph +//! keeps its own pitch (`\pxsm` / `\psm`). A height change starts `\H;`. + +use codec::entities::{ + AttachmentPoint, DrawingDirection, LineSpacingStyle, MText, TextHorizontalAlignment as HA, +}; +use codec::types::Vector3; +use codec::{CadDocument, EntityType, Handle}; + +use crate::app::settings::Txt2MtxtSettings; +use crate::entities::text_support::{resolve_dxf_special_chars, text_local_bounds}; + +/// What the command does to the drawing. +pub struct Txt2MtxtPlan { + pub erase: Vec, + pub add: Vec, +} + +/// One TEXT or MTEXT of the selection, in its own frame. +struct Piece<'a> { + handle: Handle, + entity: &'a EntityType, + /// MTEXT-encoded value. + content: String, + height: f64, + /// The frame's origin (WCS) and direction. + origin: [f64; 2], + rotation: f64, + /// Box in the frame: left, right, last baseline, top. + rect: [f64; 4], +} + +impl Piece<'_> { + fn point(&self, x: f64, y: f64) -> [f64; 2] { + let (s, c) = self.rotation.sin_cos(); + [ + self.origin[0] + x * c - y * s, + self.origin[1] + x * s + y * c, + ] + } + + fn top_left(&self) -> [f64; 2] { + self.point(self.rect[0], self.rect[3]) + } + + fn corners(&self) -> [[f64; 2]; 4] { + let [l, r, b, t] = self.rect; + [ + self.point(l, b), + self.point(r, b), + self.point(r, t), + self.point(l, t), + ] + } + + /// First and last baselines, as heights in a frame turned by `angle`. + fn baselines(&self, angle: f64) -> (f64, f64) { + let [l, _, b, t] = self.rect; + ( + up(self.point(l, t - self.height), angle), + up(self.point(l, b), angle), + ) + } +} + +/// Height of a WCS point in a frame turned by `angle`. +fn up(p: [f64; 2], angle: f64) -> f64 { + let (s, c) = angle.sin_cos(); + -p[0] * s + p[1] * c +} + +/// A TEXT value as MTEXT content: `%%u` / `%%o` become underline / overline +/// switches, braces are escaped, other `%%` codes stay, and a backslash is +/// dropped unless it starts a `\U+` / `\M+` character code. +fn text_content(value: &str) -> String { + let chars: Vec = value.chars().collect(); + let (mut under, mut over) = (false, false); + let mut out = String::new(); + let mut i = 0; + while i < chars.len() { + let ch = chars[i]; + let next = chars.get(i + 1).copied(); + if ch == '%' && next == Some('%') { + match chars.get(i + 2).map(|c| c.to_ascii_lowercase()) { + Some('u') => { + out.push_str(if under { "\\l" } else { "\\L" }); + under = !under; + i += 3; + continue; + } + Some('o') => { + out.push_str(if over { "\\o" } else { "\\O" }); + over = !over; + i += 3; + continue; + } + _ => {} + } + } + match ch { + '{' | '}' => { + out.push('\\'); + out.push(ch); + } + '\\' if matches!(next, Some('U' | 'M')) && chars.get(i + 2) == Some(&'+') => { + out.push(ch) + } + '\\' => {} + _ => out.push(ch), + } + i += 1; + } + out +} + +fn piece<'a>(handle: Handle, entity: &'a EntityType, doc: &CadDocument) -> Option> { + match entity { + EntityType::Text(t) => { + let p = crate::entities::text::text_run_placement_at_scale(t, doc, 1.0); + let advance = text_local_bounds( + &p.font, + &resolve_dxf_special_chars(&t.value), + p.height, + p.width_factor, + p.oblique_angle, + ) + .map_or(0.0, |b| b.advance.abs() as f64); + Some(Piece { + handle, + entity, + content: text_content(&t.value), + height: p.height as f64, + origin: p.origin, + rotation: p.rotation as f64, + rect: [0.0, advance, 0.0, p.height as f64], + }) + } + EntityType::MText(m) => { + let layout = crate::entities::mtext::layout_of(m, doc, false); + let width = layout.line_widths.iter().fold(0.0_f32, |a, w| a.max(*w)) as f64; + let anchor = match m.attachment_point { + AttachmentPoint::TopCenter + | AttachmentPoint::MiddleCenter + | AttachmentPoint::BottomCenter => 0.5, + AttachmentPoint::TopRight + | AttachmentPoint::MiddleRight + | AttachmentPoint::BottomRight => 1.0, + _ => 0.0, + }; + // The block runs from the first line's cap height down to the + // last baseline, hung from the attachment point. + let lines = layout.line_count.max(1); + let block = m.height + (lines - 1) as f64 * layout.line_height as f64; + let top = match m.attachment_point { + AttachmentPoint::MiddleLeft + | AttachmentPoint::MiddleCenter + | AttachmentPoint::MiddleRight => block / 2.0, + AttachmentPoint::BottomLeft + | AttachmentPoint::BottomCenter + | AttachmentPoint::BottomRight => block, + _ => 0.0, + }; + let left = -anchor * width; + Some(Piece { + handle, + entity, + content: m.value.clone(), + height: m.height, + origin: [m.insertion_point.x, m.insertion_point.y], + rotation: m.rotation, + rect: [left, left + width, top - block, top], + }) + } + _ => None, + } +} + +/// A new MTEXT with `source`'s layer, colour and style. +fn mtext_from(source: &EntityType, value: String, at: [f64; 2], height: f64, rotation: f64) -> MText { + let mut m = MText::new(); + let (common, style, z, normal) = match source { + EntityType::Text(t) => (&t.common, &t.style, t.insertion_point.z, t.normal), + EntityType::MText(t) => (&t.common, &t.style, t.insertion_point.z, t.normal), + _ => unreachable!("pieces are TEXT or MTEXT"), + }; + m.common = common.clone(); + m.common.handle = Handle::NULL; + m.common.reactors.clear(); + m.common.xdictionary_handle = None; + m.style = style.clone(); + m.normal = normal; + m.value = value; + m.insertion_point = Vector3::new(at[0], at[1], z); + m.height = height; + m.rotation = rotation; + m.rectangle_width = 0.0; + m.attachment_point = AttachmentPoint::TopLeft; + m.drawing_direction = DrawingDirection::ByStyle; + m.line_spacing_style = LineSpacingStyle::AtLeast; + m.line_spacing_factor = 1.0; + m +} + +/// Each TEXT and each line of an MTEXT as an MTEXT of its own. A TEXT keeps +/// its horizontal justification as a top attachment. +fn separate(p: &Piece, doc: &CadDocument) -> Vec { + match p.entity { + EntityType::Text(t) => { + let (x, attachment) = match t.horizontal_alignment { + HA::Center | HA::Middle => (0.5, AttachmentPoint::TopCenter), + HA::Right => (1.0, AttachmentPoint::TopRight), + _ => (0.0, AttachmentPoint::TopLeft), + }; + let at = p.point(p.rect[0] + x * (p.rect[1] - p.rect[0]), p.rect[3]); + let mut m = mtext_from(p.entity, p.content.clone(), at, p.height, p.rotation); + m.attachment_point = attachment; + vec![EntityType::MText(m)] + } + EntityType::MText(m) => { + // The drawn words, grouped into lines by their baseline. + // ponytail: a line keeps only its words, not inline formatting. + let words = crate::entities::mtext::explode_mtext(m, doc); + let mut lines: Vec<(f64, Vec<(f64, &codec::entities::Text)>)> = Vec::new(); + for word in &words { + let EntityType::Text(t) = word else { continue }; + let at = [t.insertion_point.x, t.insertion_point.y]; + let (s, c) = m.rotation.sin_cos(); + let along = at[0] * c + at[1] * s; + let v = up(at, m.rotation); + match lines + .iter_mut() + .find(|(base, _)| (base - v).abs() < m.height * 0.25) + { + Some((_, line)) => line.push((along, t)), + None => lines.push((v, vec![(along, t)])), + } + } + lines.sort_by(|a, b| b.0.total_cmp(&a.0)); + // Line k hangs one line step below line k-1 from the block's top. + let step = (p.rect[3] - p.rect[2] - p.height) / (lines.len().max(2) - 1) as f64; + let origin_along = { + let (s, c) = m.rotation.sin_cos(); + p.origin[0] * c + p.origin[1] * s + }; + lines + .into_iter() + .enumerate() + .map(|(k, (_, mut line))| { + line.sort_by(|a, b| a.0.total_cmp(&b.0)); + let value = line + .iter() + .map(|(_, t)| t.value.as_str()) + .collect::>() + .join(" "); + let at = p.point(line[0].0 - origin_along, p.rect[3] - k as f64 * step); + EntityType::MText(mtext_from(p.entity, value, at, line[0].1.height, m.rotation)) + }) + .collect() + } + _ => Vec::new(), + } +} + +/// The edit for `selected` (in selection order); `None` when nothing in it is +/// text. +pub fn plan( + selected: &[(Handle, &EntityType)], + doc: &CadDocument, + settings: Txt2MtxtSettings, +) -> Option { + let mut pieces: Vec = selected + .iter() + .filter_map(|(h, e)| piece(*h, e, doc)) + .collect(); + let frame = pieces.first()?.rotation; + if !settings.combine { + return Some(Txt2MtxtPlan { + erase: pieces.iter().map(|p| p.handle).collect(), + add: pieces.iter().flat_map(|p| separate(p, doc)).collect(), + }); + } + if !settings.select_order { + // Top-down; a tie keeps the selection order. + let top = |p: &Piece| p.corners().map(|c| up(c, frame)).into_iter().fold(f64::MIN, f64::max); + pieces.sort_by(|a, b| top(b).total_cmp(&top(a))); + } + let first = &pieces[0]; + let frame = first.rotation; + // Each paragraph's pitch to the next one, as a factor of the default + // step (5/3 of its text height). + let pitches: Vec = pieces + .windows(2) + .map(|w| { + let gap = w[0].baselines(frame).1 - w[1].baselines(frame).0; + (gap / (w[0].height * 5.0 / 3.0)).clamp(0.25, 4.0) + }) + .collect(); + let per_paragraph = !settings.word_wrap && !settings.uniform_spacing; + let mut value = String::new(); + let mut current = first.height; + for (i, p) in pieces.iter().enumerate() { + if i > 0 { + value.push_str(if settings.word_wrap { " " } else { "\\P" }); + } + if let (true, Some(pitch)) = (per_paragraph, pitches.get(i)) { + let x = if i == 0 { "x" } else { "" }; + value.push_str(&format!("\\p{x}sm{pitch:.2};")); + } + if (p.height - current).abs() > 1e-9 { + value.push_str(&format!("\\H{:.4};", p.height)); + current = p.height; + } + value.push_str(&p.content); + } + let mut m = mtext_from(first.entity, value, first.top_left(), first.height, frame); + if settings.word_wrap { + // The objects' WCS bounding box, measured along the first one. + let mut bounds = [f64::MAX, f64::MAX, f64::MIN, f64::MIN]; + for c in pieces.iter().flat_map(|p| p.corners()) { + bounds = [ + bounds[0].min(c[0]), + bounds[1].min(c[1]), + bounds[2].max(c[0]), + bounds[3].max(c[1]), + ]; + } + let (s, c) = frame.sin_cos(); + let along = [ + [bounds[0], bounds[1]], + [bounds[2], bounds[1]], + [bounds[2], bounds[3]], + [bounds[0], bounds[3]], + ] + .map(|p| p[0] * c + p[1] * s); + let span = along.iter().fold(f64::MIN, |a, v| a.max(*v)) + - along.iter().fold(f64::MAX, |a, v| a.min(*v)); + m.rectangle_width = span * 1.005; + // An even pitch carries over; an uneven one falls back to single. + if let Some(&pitch) = pitches.first() { + if pitches.iter().all(|p| (p - pitch).abs() < 1e-4) { + m.line_spacing_factor = pitch; + } + } + } + Some(Txt2MtxtPlan { + erase: pieces.iter().map(|p| p.handle).collect(), + add: vec![EntityType::MText(m)], + }) +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["TXT2MTXT"] +}); diff --git a/src/modules/draw/select.rs b/src/modules/draw/select.rs index 76417cfb5..392ecf54f 100644 --- a/src/modules/draw/select.rs +++ b/src/modules/draw/select.rs @@ -44,6 +44,8 @@ pub struct SelectObjectsCommand { /// The kinds of object the command takes; the host drops the rest of /// each completed selection silently, and Enter on nothing ends quietly. keep: Option bool>, + /// A SEttings keyword at the prompt, and what it does. + settings: Option CmdResult>, } impl SelectObjectsCommand { @@ -61,6 +63,7 @@ impl SelectObjectsCommand { prompt_text: None, associative_dimensions_only: false, keep: None, + settings: None, } } @@ -79,6 +82,7 @@ impl SelectObjectsCommand { prompt_text: None, associative_dimensions_only: false, keep: None, + settings: None, } } @@ -97,6 +101,7 @@ impl SelectObjectsCommand { prompt_text: None, associative_dimensions_only: false, keep: None, + settings: None, } } @@ -115,6 +120,7 @@ impl SelectObjectsCommand { prompt_text: None, associative_dimensions_only: false, keep: None, + settings: None, } } @@ -150,6 +156,19 @@ impl SelectObjectsCommand { command } + /// The command's own wording with a SEttings keyword; Enter on nothing + /// ends quietly. + pub fn with_settings( + prompt_cmd: &str, + pending_cmd: &str, + prompt_text: &'static str, + settings: fn() -> CmdResult, + ) -> Self { + let mut command = Self::filtered(prompt_cmd, pending_cmd, prompt_text, |_| true); + command.settings = Some(settings); + command + } + pub fn auto_constrain(pending_cmd: &str) -> Self { let mut command = Self::new(pending_cmd); command.auto_constrain_settings = true; @@ -242,6 +261,9 @@ impl CadCommand for SelectObjectsCommand { // they work typed as well — these buttons only surface them, which is what // the on-screen keyboard-less case needs. fn options(&self) -> Vec { + if self.settings.is_some() && self.pick.is_none() { + return vec![CmdOption::new("SEttings", "SE")]; + } if self.commit_on_enter && self.show_options { if self.auto_constrain_settings { return vec![CmdOption::new(t!("Settings").as_ref(), "S")]; @@ -297,6 +319,12 @@ impl CadCommand for SelectObjectsCommand { if !self.commit_on_enter { return None; } + let word = text.trim().trim_start_matches('_').to_uppercase(); + if let Some(settings) = self.settings { + if word.len() >= 2 && "SETTINGS".starts_with(word.as_str()) { + return Some(settings()); + } + } match text.trim().to_uppercase().as_str() { "S" | "SETTINGS" if self.auto_constrain_settings => { Some(CmdResult::OpenAutoConstrainSettings) diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs index 1afa5446b..957417eef 100644 --- a/src/ui/window/pdf_dialogs.rs +++ b/src/ui/window/pdf_dialogs.rs @@ -101,6 +101,13 @@ pub enum PdfDialogMsg { Lineweights(bool), Linetypes(bool), SettingsOk, + // Text to MText Settings (TXT2MTXT) + T2mCombine(bool), + /// Keep the selection order (else top-down). + T2mSelectOrder(bool), + T2mWrap(bool), + T2mUniform(bool), + T2mOk, // Import PDF ImportBrowse, ImportPage(usize), @@ -1261,6 +1268,194 @@ pub fn view_import_settings<'a>( .into() } +// ── Text to MText Settings (TXT2MTXT) ────────────────────────────────────── + +/// A labelled switch; inactive (dimmed) while `on` is None. +fn t2m_switch<'a>( + label: String, + hint: String, + value: bool, + on: Option PdfDialogMsg>, +) -> Element<'a, Message> { + let enabled = on.is_some(); + let mut switch = iced::widget::toggler(value).size(18.0); + if let Some(on) = on { + switch = switch.on_toggle(move |v| msg(on(v))); + } + let dim = move |theme: &Theme| text::Style { + color: Some(theme.palette().background.strong.color), + }; + let name = text(label).size(12); + let name = if enabled { name } else { name.style(dim) }; + row![ + switch, + name, + text(hint).size(11).style(if enabled { muted_style } else { dim }), + ] + .spacing(8) + .align_y(iced::Center) + .into() +} + +/// Top-down or selection order, as joined buttons; inactive without `enabled`. +fn t2m_order<'a>(select_order: bool, enabled: bool) -> Element<'a, Message> { + let choice = |value: bool, label: String| -> Element<'a, Message> { + let chosen = enabled && value == select_order; + button(text(label).size(11).width(Fill).align_x(iced::Center)) + .on_press_maybe(enabled.then(|| msg(PdfDialogMsg::T2mSelectOrder(value)))) + .style(move |theme: &Theme, status| { + let mut style = button_style(chosen)(theme, status); + if !enabled { + style.text_color = theme.palette().background.strong.color; + } + style + }) + .padding([5, 6]) + .width(Fill) + .into() + }; + container( + row![ + choice(false, crate::tr!("txt2mtxt", "top-down")), + choice(true, crate::tr!("txt2mtxt", "selection-order")) + ] + .spacing(2), + ) + .padding(2) + .width(Length::Fixed(300.0)) + .style(well_style) + .into() +} + +/// A text sample in a well, one line per entry. +fn t2m_sample<'a>(lines: Vec, framed: bool, width: f32) -> Element<'a, Message> { + let body = iced::widget::Column::with_children( + lines + .into_iter() + .map(|l| text(l).size(12).font(iced::Font::MONOSPACE).into()) + .collect::>(), + ) + .spacing(6); + let inner = container(body).padding([6, 8]).width(Fill); + let inner: Element<'a, Message> = if framed { + inner + .style(|theme: &Theme| container::Style { + border: Border { + width: 1.0, + radius: 2.0.into(), + color: theme.palette().primary.base.color, + }, + ..Default::default() + }) + .into() + } else { + inner.into() + }; + container(inner) + .padding(6) + .width(Length::Fixed(width)) + .height(Length::Fixed(86.0)) + .style(well_style) + .into() +} + +pub fn view_txt2mtxt_settings<'a>( + s: &'a crate::app::settings::Txt2MtxtSettings, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let result = card( + crate::tr!("txt2mtxt", "result"), + column![ + t2m_switch( + crate::tr!("txt2mtxt", "combine"), + String::new(), + s.combine, + Some(PdfDialogMsg::T2mCombine), + ), + column![ + text(crate::tr!("txt2mtxt", "order")).size(11).style(muted_style), + t2m_order(s.select_order, s.combine), + ] + .spacing(6) + .padding(iced::Padding::ZERO.left(44)), + text(crate::tr!("txt2mtxt", "combine-note")) + .size(11) + .style(muted_style), + ] + .spacing(8), + ); + let lines = card( + crate::tr!("txt2mtxt", "lines"), + column![ + t2m_switch( + crate::tr!("txt2mtxt", "wrap"), + crate::tr!("txt2mtxt", "wrap-hint"), + s.word_wrap, + s.combine.then_some(PdfDialogMsg::T2mWrap as fn(bool) -> PdfDialogMsg), + ), + t2m_switch( + crate::tr!("txt2mtxt", "uniform"), + crate::tr!("txt2mtxt", "uniform-hint"), + s.uniform_spacing || s.word_wrap, + (s.combine && !s.word_wrap) + .then_some(PdfDialogMsg::T2mUniform as fn(bool) -> PdfDialogMsg), + ), + ] + .spacing(10), + ); + let sample: Vec = ["First line", "Second line", "Third line"] + .into_iter() + .map(String::from) + .collect(); + let (count, out) = if !s.combine { + (3, sample.clone()) + } else if s.word_wrap { + ( + 1, + vec!["First line Second line Third".into(), "line".into()], + ) + } else { + (1, sample.clone()) + }; + let preview = container( + row![ + column![ + text(crate::tr!("txt2mtxt", "selected")).size(11).style(muted_style), + t2m_sample(sample, false, 190.0) + ] + .spacing(4), + text("→").size(20).style(accent_text), + column![ + text(crate::tr!("txt2mtxt", "result-count", count = count)) + .size(11) + .style(muted_style), + t2m_sample(out, true, 262.0) + ] + .spacing(4), + ] + .spacing(14) + .align_y(iced::Center), + ) + .padding([10, 12]) + .width(Fill) + .style(card_style); + column![ + result, + lines, + preview, + footer( + "txt2mtxt", + None, + crate::tr!("txt2mtxt", "apply").into(), + PdfDialogMsg::T2mOk, + ), + ] + .spacing(10) + .padding([10, 12]) + .width(sizing.width) + .into() +} + // ── Import PDF (File) ────────────────────────────────────────────────────── pub struct PdfImportFileState {