Merge pull request #1811 from ramox81/codex/txt2mtxt

TXT2MTXT: combine text into MText with a settings dialog

# Conflicts:
#	locales/ar-SA/opencadstudio.ftl
#	locales/bg-BG/opencadstudio.ftl
#	locales/cs-CZ/opencadstudio.ftl
#	locales/de-DE/opencadstudio.ftl
#	locales/el-GR/opencadstudio.ftl
#	locales/en-US/opencadstudio.ftl
#	locales/es-ES/opencadstudio.ftl
#	locales/fi-FI/opencadstudio.ftl
#	locales/fr-FR/opencadstudio.ftl
#	locales/hi-IN/opencadstudio.ftl
#	locales/hu-HU/opencadstudio.ftl
#	locales/it-IT/opencadstudio.ftl
#	locales/ja-JP/opencadstudio.ftl
#	locales/ko-KR/opencadstudio.ftl
#	locales/lo-LA/opencadstudio.ftl
#	locales/nl-NL/opencadstudio.ftl
#	locales/pl-PL/opencadstudio.ftl
#	locales/pt-BR/opencadstudio.ftl
#	locales/ru-RU/opencadstudio.ftl
#	locales/tr-TR/opencadstudio.ftl
#	locales/zh-CN/opencadstudio.ftl
#	locales/zh-TW/opencadstudio.ftl
This commit is contained in:
Hakan Seven committed 2026-10-15 16:47:19 +03:00
commit aa618b8327
40 files changed
+1222 -4

No files matched your search

+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+17
View File
@@ -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.
+1
View File
@@ -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();
}
+73
View File
@@ -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::<Vec<_>>()
.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<codec::Handle> = 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.")
});
}
}
+4
View File
@@ -779,6 +779,10 @@ inventory::submit!(crate::command::CommandRegistration {
"UCSDETECT",
"SURFACEASSOCIATIVITY",
"LAYOUTCREATEVIEWPORT",
"TXT2MTXTCOMBINEMTEXT",
"TXT2MTXTTEXTORDER",
"TXT2MTXTWORDWRAP",
"TXT2MTXTLINESPACING",
"FIELDEVAL",
"PDFIMPORTMODE",
"PDFIMPORTFILTER",
+38
View File
@@ -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) => {
+48
View File
@@ -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::<i16>().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
+22
View File
@@ -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 {
+1
View File
@@ -381,6 +381,7 @@ fn command_selection_policy(name: &str) -> &'static str {
"ERASE",
"EXPLODE",
"TXTEXP",
"TXT2MTXT",
"GROUP",
"LAYFRZ",
"LAYLCK",
+8
View File
@@ -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<settings::Txt2MtxtSettings>,
auto_constrain_saved: Option<settings::AutoConstrainSettings>,
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(),
+32
View File
@@ -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,
+2
View File
@@ -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);
+11
View File
@@ -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,
+7
View File
@@ -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| {
+3
View File
@@ -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
+13 -4
View File
@@ -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<GlyphBox> {
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<GlyphBox> {
// 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<GlyphBox> {
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 {
+1
View File
@@ -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};
+361
View File
@@ -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<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<Handle>,
pub add: Vec<EntityType>,
}
/// 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<char> = 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<Piece<'a>> {
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<EntityType> {
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::<Vec<_>>()
.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<Txt2MtxtPlan> {
let mut pieces: Vec<Piece> = 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<f64> = 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"]
});
+28
View File
@@ -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<fn(&codec::EntityType) -> bool>,
/// A SEttings keyword at the prompt, and what it does.
settings: Option<fn() -> 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<CmdOption> {
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)
+195
View File
@@ -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<fn(bool) -> 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<String>, 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::<Vec<_>>(),
)
.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<String> = ["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 {