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

+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| {