forked from 1b-agent/opencadstudio-upstream
TXT2MTXT: combine text into MText, with its settings dialog
TEXT and MTEXT objects are merged into one MTEXT (top-down or in selection order, word-wrapped or one paragraph per object, with the original pitch kept as line spacing) or each TEXT and MTEXT line becomes an MTEXT of its own. The SEttings option opens the Combine Text settings dialog; the settings persist as profile values and are exposed as the TXT2MTXT* variables and the txt2mtxt_settings automation action. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
39c3e9af58
commit
4e4dc86908
40 files changed
+1244
-4
No files matched your search
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -770,6 +770,10 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"SELECTIONCYCLING",
|
||||
"SURFACEASSOCIATIVITY",
|
||||
"LAYOUTCREATEVIEWPORT",
|
||||
"TXT2MTXTCOMBINEMTEXT",
|
||||
"TXT2MTXTTEXTORDER",
|
||||
"TXT2MTXTWORDWRAP",
|
||||
"TXT2MTXTLINESPACING",
|
||||
"FIELDEVAL",
|
||||
"PDFIMPORTMODE",
|
||||
"PDFIMPORTFILTER",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1281,6 +1281,10 @@ impl OpenCADStudio {
|
||||
| "DYNCONSTRAINTDISPLAY"
|
||||
| "CCONSTRAINTFORM"
|
||||
| "DYNMODE"
|
||||
| "TXT2MTXTCOMBINEMTEXT"
|
||||
| "TXT2MTXTTEXTORDER"
|
||||
| "TXT2MTXTWORDWRAP"
|
||||
| "TXT2MTXTLINESPACING"
|
||||
) =>
|
||||
{
|
||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||
@@ -1577,6 +1581,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
|
||||
|
||||
@@ -103,6 +103,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",
|
||||
@@ -2088,6 +2089,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 {
|
||||
|
||||
@@ -381,6 +381,7 @@ fn command_selection_policy(name: &str) -> &'static str {
|
||||
"ERASE",
|
||||
"EXPLODE",
|
||||
"TXTEXP",
|
||||
"TXT2MTXT",
|
||||
"GROUP",
|
||||
"LAYFRZ",
|
||||
"LAYLCK",
|
||||
|
||||
@@ -764,6 +764,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,
|
||||
@@ -2178,6 +2182,8 @@ pub enum ModalKind {
|
||||
RefEdit,
|
||||
UnderlayLayers,
|
||||
PdfImportSettings,
|
||||
/// TXT2MTXT's Text to MText Settings, over the running command.
|
||||
Txt2MtxtSettings,
|
||||
PdfImportFile,
|
||||
XrefAttach,
|
||||
WriteBlock,
|
||||
@@ -4693,6 +4699,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(),
|
||||
|
||||
@@ -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 {
|
||||
@@ -568,6 +596,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,
|
||||
@@ -962,6 +993,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,
|
||||
|
||||
@@ -862,6 +862,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,
|
||||
@@ -1031,6 +1032,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);
|
||||
|
||||
@@ -237,6 +237,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,
|
||||
|
||||
@@ -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(),
|
||||
@@ -575,6 +576,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| {
|
||||
|
||||
Reference in new issue
Block a user