forked from 1b-agent/opencadstudio-upstream
fix(dimarc): complete merge integration
Preserve source-plane geometry and associative arc parameters while combining the latest properties and tolerance workflows.
This commit is contained in:
commit
6ef2d75e84
67 files changed
+4773
-883
No files matched your search
@@ -193,6 +193,17 @@ fn entity_json(e: &acadrust::EntityType) -> Value {
|
||||
impl OpenCADStudio {
|
||||
/// Handle one JSON request line and return the JSON response.
|
||||
pub(crate) fn automation_op(&mut self, line: &str) -> Value {
|
||||
let res = self.automation_op_inner(line);
|
||||
// Most automation ops mutate `Scene::selected` directly rather than
|
||||
// going through `update()` (`select` calls `deselect_all` /
|
||||
// `select_entity`, and `run` can erase the selected entities), so the
|
||||
// selection check has to run on this path too.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.notify_plugins_selection_changed();
|
||||
res
|
||||
}
|
||||
|
||||
fn automation_op_inner(&mut self, line: &str) -> Value {
|
||||
let req: Value = match serde_json::from_str(line) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return err(format!("invalid JSON: {e}")),
|
||||
@@ -201,6 +212,7 @@ impl OpenCADStudio {
|
||||
"new" => {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.document = acadrust::CadDocument::new();
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].current_path = None;
|
||||
// The headless session starts on the welcome (Start) tab, which
|
||||
// blocks drawing commands; turn it into a real drawing.
|
||||
@@ -224,6 +236,7 @@ impl OpenCADStudio {
|
||||
Ok(doc) => {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.document = doc;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
crate::app::style_ops::ensure_standard_styles(
|
||||
&mut self.tabs[i].scene.document,
|
||||
);
|
||||
|
||||
@@ -3716,10 +3716,11 @@ impl OpenCADStudio {
|
||||
handle,
|
||||
initial,
|
||||
height,
|
||||
template,
|
||||
} => {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.open_mtext_editor(pos, handle, &initial, height);
|
||||
self.open_mtext_editor(pos, handle, &initial, height, template.map(|m| *m));
|
||||
}
|
||||
CmdResult::SuspendForMTextInput {
|
||||
pos,
|
||||
@@ -3732,7 +3733,7 @@ impl OpenCADStudio {
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_mtext_input = true;
|
||||
self.pending_command_editor_text = None;
|
||||
self.open_mtext_editor(pos, None, &initial, height);
|
||||
self.open_mtext_editor(pos, None, &initial, height, None);
|
||||
}
|
||||
CmdResult::OpenTextEditor {
|
||||
pos,
|
||||
@@ -3748,6 +3749,22 @@ impl OpenCADStudio {
|
||||
&initial,
|
||||
height,
|
||||
super::text_inline::TextEntityField::Text,
|
||||
None,
|
||||
);
|
||||
}
|
||||
CmdResult::SuspendForTextInput { pos, entity } => {
|
||||
self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take();
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
let height = entity.height;
|
||||
self.open_text_inline(
|
||||
pos,
|
||||
None,
|
||||
"",
|
||||
height,
|
||||
super::text_inline::TextEntityField::Text,
|
||||
Some(entity),
|
||||
);
|
||||
}
|
||||
CmdResult::EditTextEntity { handle } => {
|
||||
|
||||
@@ -601,10 +601,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"TOLERANCE" => {
|
||||
use crate::modules::annotate::tolerance_cmd::ToleranceCommand;
|
||||
let cmd = ToleranceCommand::new();
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
self.open_tolerance_dialog(None);
|
||||
}
|
||||
|
||||
"TABLE" => {
|
||||
|
||||
@@ -1268,11 +1268,16 @@ impl OpenCADStudio {
|
||||
// ── Annotate commands ──────────────────────────────────────────
|
||||
"TEXT" => {
|
||||
use crate::modules::annotate::text::TextCommand;
|
||||
let height = crate::scene::creation_style::current_text_defaults(
|
||||
&self.tabs[i].scene.document,
|
||||
)
|
||||
.height;
|
||||
let new_cmd = TextCommand::with_height(height);
|
||||
let (defaults, styles, annotation_multiplier) = {
|
||||
let scene = &self.tabs[i].scene;
|
||||
let annotation_multiplier = scene.creation_annotation_multiplier();
|
||||
let defaults =
|
||||
crate::scene::creation_style::current_text_defaults(&scene.document);
|
||||
let styles = scene.document.text_styles.iter().cloned().collect();
|
||||
(defaults, styles, annotation_multiplier)
|
||||
};
|
||||
let new_cmd =
|
||||
TextCommand::with_defaults(defaults, styles, annotation_multiplier);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
@@ -1307,7 +1312,13 @@ impl OpenCADStudio {
|
||||
&self.tabs[i].scene.document,
|
||||
)
|
||||
.height;
|
||||
let new_cmd = MTextCommand::with_height(height);
|
||||
let style = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.current_text_style_name
|
||||
.clone();
|
||||
let new_cmd = MTextCommand::with_defaults(height, style);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
@@ -943,7 +943,12 @@ impl OpenCADStudio {
|
||||
|
||||
"ALIGN" => {
|
||||
use crate::modules::draw::modify::align::AlignCommand;
|
||||
let cmd = AlignCommand::new();
|
||||
|
||||
let selected: Vec<acadrust::Handle> =
|
||||
self.tabs[i].scene.selected.iter().copied().collect();
|
||||
|
||||
let cmd = AlignCommand::with_selection(selected);
|
||||
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ pub(crate) mod settings;
|
||||
mod shortcuts;
|
||||
mod style_ops;
|
||||
mod text_inline;
|
||||
mod tolerance_dialog;
|
||||
mod update;
|
||||
mod view;
|
||||
mod visibility;
|
||||
@@ -399,6 +400,8 @@ pub(super) struct OpenCADStudio {
|
||||
layer_translator: Option<crate::ui::window::layer_translator::State>,
|
||||
/// Working copy of the Drawing Units dialog; `None` while it is closed.
|
||||
drawing_units: Option<crate::ui::window::drawing_units::State>,
|
||||
/// Working copy of the structured feature-control-frame editor.
|
||||
geometric_tolerance: Option<crate::ui::window::geometric_tolerance::State>,
|
||||
/// PICKDRAG (#226): false (default) = press-drag lassoes; true =
|
||||
/// press-drag draws a rectangle marquee.
|
||||
pick_drag_rect: bool,
|
||||
@@ -691,6 +694,10 @@ pub(super) struct OpenCADStudio {
|
||||
/// keep their manifest listed but drop their ribbon tab and command
|
||||
/// dispatch. Persisted via [`settings::UserSettings::disabled_plugins`].
|
||||
disabled_plugins: rustc_hash::FxHashSet<String>,
|
||||
/// `(tab id, selection fingerprint)` last broadcast to V4 plugins, so
|
||||
/// `SelectionChangedV4` fires once per real change rather than per message.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
last_plugin_selection: Option<(u64, u64)>,
|
||||
/// External add-on packages found in the plugins folder, refreshed when the
|
||||
/// Plugin Manager opens.
|
||||
external_plugins: Vec<crate::plugin::external::ExternalPlugin>,
|
||||
@@ -1588,6 +1595,7 @@ pub enum ModalKind {
|
||||
LayerStateManager,
|
||||
LayerTranslator,
|
||||
DrawingUnits,
|
||||
GeometricTolerance,
|
||||
DraftingSettings,
|
||||
LayerStateEditor,
|
||||
Plot,
|
||||
@@ -2318,6 +2326,14 @@ pub enum Message {
|
||||
DrawingUnitsField(crate::ui::window::drawing_units::Field),
|
||||
/// Drawing Units OK — write the working copy into the drawing.
|
||||
DrawingUnitsApply,
|
||||
/// One structured feature-control-frame field changed.
|
||||
ToleranceDialogField(crate::ui::window::geometric_tolerance::Field),
|
||||
/// One structured feature-control-frame option changed.
|
||||
ToleranceDialogToggle(crate::ui::window::geometric_tolerance::Toggle),
|
||||
/// Apply edits without closing the structured editor.
|
||||
ToleranceDialogApply,
|
||||
/// Commit edits or continue to insertion-point placement.
|
||||
ToleranceDialogOk,
|
||||
/// Toggle the Isolate pill's action menu open/closed.
|
||||
ToggleIsolatePopup,
|
||||
/// Close the Isolate action menu.
|
||||
@@ -2655,6 +2671,31 @@ pub enum Message {
|
||||
MTextWidth(String),
|
||||
/// Toolbar character-spacing field changed.
|
||||
MTextCharSpace(String),
|
||||
/// Undo / redo editor text and inline-format operations.
|
||||
MTextUndo,
|
||||
MTextRedo,
|
||||
/// Convert the selected numerator/separator/denominator to a stacked run.
|
||||
MTextStack,
|
||||
/// Remove inline character formatting from the selection (or all text).
|
||||
MTextClearFormatting,
|
||||
/// Insert a predefined symbol or field token at the caret.
|
||||
MTextInsert(String),
|
||||
/// Per-object annotation flag edited from the text toolbar.
|
||||
MTextAnnotative(bool),
|
||||
/// Column layout controls.
|
||||
MTextColumnMode(String),
|
||||
MTextColumnCount(String),
|
||||
MTextColumnWidth(String),
|
||||
MTextColumnGutter(String),
|
||||
MTextColumnHeight(String),
|
||||
MTextColumnFlowReversed(bool),
|
||||
/// Paragraph indent/spacing controls.
|
||||
MTextParagraphNumber(mtext_editor::ParaNumber, String),
|
||||
MTextFindText(String),
|
||||
MTextReplaceText(String),
|
||||
MTextFindNext,
|
||||
MTextReplaceNext,
|
||||
MTextReplaceAll,
|
||||
/// Toolbar colour picker (same widget as Properties) — applies to the
|
||||
/// selection, or the whole text when nothing is selected.
|
||||
MTextColorChanged(AcadColor),
|
||||
@@ -3145,6 +3186,7 @@ impl OpenCADStudio {
|
||||
last_layer_translation: None,
|
||||
layer_translator: None,
|
||||
drawing_units: None,
|
||||
geometric_tolerance: None,
|
||||
pick_drag_rect: false,
|
||||
perf_hud: false,
|
||||
cycle_candidates: None,
|
||||
@@ -3246,6 +3288,8 @@ impl OpenCADStudio {
|
||||
attr_editor_tab: crate::ui::window::attribute_editor::AttrTab::Attribute,
|
||||
attr_editor_selected: 0,
|
||||
disabled_plugins: rustc_hash::FxHashSet::default(),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
last_plugin_selection: None,
|
||||
external_plugins: Vec::new(),
|
||||
loaded_plugin_ids: rustc_hash::FxHashSet::default(),
|
||||
plugin_load_errors: rustc_hash::FxHashMap::default(),
|
||||
|
||||
+489
-19
@@ -8,7 +8,7 @@
|
||||
use acadrust::entities::mtext::AttachmentPoint;
|
||||
use acadrust::entities::mtext_format::{
|
||||
parse_mtext, MTextColor, MTextDocument, MTextFont, MTextParagraph, MTextParagraphAlignment,
|
||||
MTextSpan, ParagraphProperties, SpanProperties, StackingData, StackingType,
|
||||
MTextScalar, MTextSpan, ParagraphProperties, SpanProperties, StackingData, StackingType,
|
||||
};
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle, MText};
|
||||
@@ -38,6 +38,15 @@ pub enum ParaAlign {
|
||||
Justify,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ParaNumber {
|
||||
FirstIndent,
|
||||
LeftIndent,
|
||||
RightIndent,
|
||||
SpaceBefore,
|
||||
SpaceAfter,
|
||||
}
|
||||
|
||||
/// `pick_list`-friendly wrapper for the 9 attachment points.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct JustifyChoice(pub AttachmentPoint);
|
||||
@@ -107,6 +116,23 @@ pub struct MTextEditorState {
|
||||
/// it is NOT derived from the typed content, so adding characters wraps
|
||||
/// to the next line instead of stretching the box into one long line.
|
||||
pub rect_width: f64,
|
||||
pub rect_height: String,
|
||||
pub annotative: bool,
|
||||
pub column_type: i16,
|
||||
pub column_count: String,
|
||||
pub column_auto_height: bool,
|
||||
pub column_flow_reversed: bool,
|
||||
pub column_width: String,
|
||||
pub column_gutter: String,
|
||||
pub paragraph_first_indent: String,
|
||||
pub paragraph_left_indent: String,
|
||||
pub paragraph_right_indent: String,
|
||||
pub paragraph_space_before: String,
|
||||
pub paragraph_space_after: String,
|
||||
pub find_text: String,
|
||||
pub replace_text: String,
|
||||
undo: Vec<String>,
|
||||
redo: Vec<String>,
|
||||
/// `Some` when editing an existing entity; `None` for a fresh MText.
|
||||
pub editing: Option<Handle>,
|
||||
/// When true the panel shows the rendered preview; when false the raw
|
||||
@@ -167,6 +193,23 @@ impl MTextEditorState {
|
||||
// Default box ~20 characters wide; overwritten with the entity's
|
||||
// own width when editing an existing MText.
|
||||
rect_width: (height * 20.0).max(1.0),
|
||||
rect_height: "0".to_string(),
|
||||
annotative: false,
|
||||
column_type: 0,
|
||||
column_count: "2".to_string(),
|
||||
column_auto_height: false,
|
||||
column_flow_reversed: false,
|
||||
column_width: (height * 10.0).to_string(),
|
||||
column_gutter: (height * 5.0).to_string(),
|
||||
paragraph_first_indent: "0".to_string(),
|
||||
paragraph_left_indent: "0".to_string(),
|
||||
paragraph_right_indent: "0".to_string(),
|
||||
paragraph_space_before: "0".to_string(),
|
||||
paragraph_space_after: "0".to_string(),
|
||||
find_text: String::new(),
|
||||
replace_text: String::new(),
|
||||
undo: Vec::new(),
|
||||
redo: Vec::new(),
|
||||
editing,
|
||||
screen_anchor: iced::Point::new(60.0, 90.0),
|
||||
original: None,
|
||||
@@ -274,8 +317,54 @@ impl MTextEditorState {
|
||||
mt.attachment_point = self.attachment;
|
||||
mt.line_spacing_factor = self.line_spacing as f64;
|
||||
mt.style = self.style.clone();
|
||||
mt.is_annotative = self.annotative;
|
||||
let rectangle_height = self.rect_height.trim().parse::<f64>().unwrap_or(0.0);
|
||||
mt.rectangle_height = (rectangle_height > 0.0).then_some(rectangle_height);
|
||||
mt.column_data.column_type = self.column_type;
|
||||
if self.column_type == 0 {
|
||||
mt.column_data.column_count = 0;
|
||||
mt.column_data.heights.clear();
|
||||
} else {
|
||||
mt.column_data.column_count = crate::entities::text_support::clamp_mtext_column_count(
|
||||
self.column_count
|
||||
.trim()
|
||||
.parse::<i32>()
|
||||
.ok()
|
||||
.filter(|count| *count > 0)
|
||||
.unwrap_or(1),
|
||||
);
|
||||
mt.column_data.auto_height = self.column_auto_height;
|
||||
mt.column_data.flow_reversed = self.column_flow_reversed;
|
||||
if let Ok(width) = self.column_width.trim().parse::<f64>() {
|
||||
if width > 0.0 {
|
||||
mt.column_data.width = width;
|
||||
}
|
||||
}
|
||||
if let Ok(gutter) = self.column_gutter.trim().parse::<f64>() {
|
||||
if gutter >= 0.0 {
|
||||
mt.column_data.gutter = gutter;
|
||||
}
|
||||
}
|
||||
if !self.column_auto_height && rectangle_height > 0.0 {
|
||||
mt.column_data.heights = vec![
|
||||
rectangle_height;
|
||||
mt.column_data.column_count as usize
|
||||
];
|
||||
}
|
||||
}
|
||||
mt
|
||||
}
|
||||
|
||||
fn record_undo(&mut self) {
|
||||
let value = self.content.text();
|
||||
if self.undo.last().is_none_or(|last| last != &value) {
|
||||
self.undo.push(value);
|
||||
if self.undo.len() > 100 {
|
||||
self.undo.remove(0);
|
||||
}
|
||||
}
|
||||
self.redo.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a numeric field, returning `Some(v)` only when it differs from the
|
||||
@@ -595,6 +684,27 @@ fn vertical_caret_target(
|
||||
best.1
|
||||
}
|
||||
|
||||
fn seed_mtext_state(state: &mut MTextEditorState, m: &MText) {
|
||||
state.attachment = m.attachment_point;
|
||||
state.line_spacing = m.line_spacing_factor as f32;
|
||||
state.rect_width = m.rectangle_width.max(0.0);
|
||||
state.rect_height = m.rectangle_height.unwrap_or(0.0).max(0.0).to_string();
|
||||
state.annotative = m.is_annotative;
|
||||
state.column_type = m.column_data.column_type;
|
||||
state.column_count = crate::entities::text_support::clamp_mtext_column_count(
|
||||
m.column_data.column_count,
|
||||
)
|
||||
.to_string();
|
||||
state.column_auto_height = m.column_data.auto_height;
|
||||
state.column_flow_reversed = m.column_data.flow_reversed;
|
||||
state.column_width = m.column_data.width.max(0.0).to_string();
|
||||
state.column_gutter = m.column_data.gutter.max(0.0).to_string();
|
||||
if !m.style.trim().is_empty() {
|
||||
state.style = m.style.clone();
|
||||
}
|
||||
state.original = Some(m.clone());
|
||||
}
|
||||
|
||||
impl super::OpenCADStudio {
|
||||
/// Open the in-place editor for a new (`handle = None`) or existing MText.
|
||||
/// Open the rich MText editor for a new or existing MText / MultiLeader.
|
||||
@@ -605,6 +715,7 @@ impl super::OpenCADStudio {
|
||||
handle: Option<Handle>,
|
||||
initial: &str,
|
||||
height: f64,
|
||||
template: Option<MText>,
|
||||
) {
|
||||
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
|
||||
return;
|
||||
@@ -614,20 +725,11 @@ impl super::OpenCADStudio {
|
||||
state.screen_anchor = p;
|
||||
}
|
||||
// Seed attachment / line-spacing / box width from the entity being edited.
|
||||
let has_template = template.is_some();
|
||||
if let Some(h) = handle {
|
||||
match self.tabs[self.active_tab].scene.document.get_entity(h) {
|
||||
Some(EntityType::MText(m)) => {
|
||||
state.attachment = m.attachment_point;
|
||||
state.line_spacing = m.line_spacing_factor as f32;
|
||||
if !m.style.trim().is_empty() {
|
||||
state.style = m.style.clone();
|
||||
}
|
||||
if m.rectangle_width > 0.0 {
|
||||
state.rect_width = m.rectangle_width;
|
||||
}
|
||||
// Keep the whole entity so `build_mtext` preserves the fields
|
||||
// the toolbar can't edit (columns, rotation, background…).
|
||||
state.original = Some(m.clone());
|
||||
seed_mtext_state(&mut state, m);
|
||||
}
|
||||
Some(EntityType::MultiLeader(ml)) => {
|
||||
state.line_spacing = ml.context.line_spacing_factor as f32;
|
||||
@@ -637,6 +739,8 @@ impl super::OpenCADStudio {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else if let Some(m) = template {
|
||||
seed_mtext_state(&mut state, &m);
|
||||
} else {
|
||||
// New MText inherits the document's current text style (STYLE),
|
||||
// not the "Standard" default. See #92.
|
||||
@@ -659,7 +763,7 @@ impl super::OpenCADStudio {
|
||||
// The preview uses a fixed on-screen text size. Therefore the initial
|
||||
// wrap width is the drawing-unit span that exactly reaches the right
|
||||
// edge of the editor, placing both ruler and slider at their maximum.
|
||||
if handle.is_none() {
|
||||
if handle.is_none() && !has_template {
|
||||
let initial_width = self.mtext_editor.as_ref().map(|ed| {
|
||||
super::view::overlay::MTEXT_EDITOR_WRITING_WIDTH / ed.preview_scale()
|
||||
});
|
||||
@@ -746,6 +850,322 @@ impl super::OpenCADStudio {
|
||||
/// Splice text around the preview selection (visible-char range) in the
|
||||
/// raw value. `case` optionally transforms the selected slice. Returns
|
||||
/// true when a preview selection was present and applied.
|
||||
pub(super) fn mtext_undo(&mut self) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
if let Some(previous) = ed.undo.pop() {
|
||||
ed.redo.push(ed.content.text());
|
||||
ed.content = text_editor::Content::with_text(&previous);
|
||||
ed.caret = previous.chars().count();
|
||||
ed.sel = Some((ed.caret, ed.caret));
|
||||
ed.sel_anchor = ed.caret;
|
||||
}
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
pub(super) fn mtext_redo(&mut self) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
if let Some(next) = ed.redo.pop() {
|
||||
ed.undo.push(ed.content.text());
|
||||
ed.content = text_editor::Content::with_text(&next);
|
||||
ed.caret = next.chars().count();
|
||||
ed.sel = Some((ed.caret, ed.caret));
|
||||
ed.sel_anchor = ed.caret;
|
||||
}
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
pub(super) fn mtext_apply_span_number(&mut self, field: &'static str, value: String) {
|
||||
let Some(number) = value.trim().replace(',', ".").parse::<f64>().ok() else {
|
||||
return;
|
||||
};
|
||||
let valid = match field {
|
||||
"height" => number > 0.0,
|
||||
"oblique" => (-85.0..=85.0).contains(&number),
|
||||
"width" => number > 0.0,
|
||||
"tracking" => number.is_finite(),
|
||||
_ => false,
|
||||
};
|
||||
if !valid {
|
||||
return;
|
||||
}
|
||||
let has_selection = self
|
||||
.mtext_editor
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.sel)
|
||||
.is_some_and(|(start, end)| start < end);
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
if has_selection {
|
||||
let (start, end) = ed.sel.unwrap();
|
||||
ed.record_undo();
|
||||
let para0 = doc_para0(&ed.doc);
|
||||
let mut cells = doc_to_cells(&ed.doc);
|
||||
let end = end.min(cells.len());
|
||||
for cell in &mut cells[start.min(end)..end] {
|
||||
if let Cell::Char(_, props) | Cell::Stack { props, .. } = cell {
|
||||
match field {
|
||||
"height" => props.height = Some(MTextScalar::Absolute(number)),
|
||||
"oblique" => props.oblique_angle = (number != 0.0).then_some(number),
|
||||
"width" => props.width_factor = (number != 1.0).then_some(number),
|
||||
"tracking" => props.tracking = (number != 0.0).then_some(number),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
ed.content = text_editor::Content::with_text(
|
||||
&cells_to_doc(¶0, &cells).to_mtext_string(),
|
||||
);
|
||||
ed.sel = Some((start, end));
|
||||
ed.caret = end;
|
||||
} else {
|
||||
match field {
|
||||
"height" => ed.height = value,
|
||||
"oblique" => ed.oblique = value,
|
||||
"width" => ed.width = value,
|
||||
"tracking" => ed.char_space = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
pub(super) fn mtext_apply_paragraph_number(&mut self, field: ParaNumber, value: String) {
|
||||
let Some(number) = value.trim().replace(',', ".").parse::<f64>().ok() else {
|
||||
return;
|
||||
};
|
||||
if !number.is_finite() {
|
||||
return;
|
||||
}
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.record_undo();
|
||||
match field {
|
||||
ParaNumber::FirstIndent => ed.paragraph_first_indent = value,
|
||||
ParaNumber::LeftIndent => ed.paragraph_left_indent = value,
|
||||
ParaNumber::RightIndent => ed.paragraph_right_indent = value,
|
||||
ParaNumber::SpaceBefore => ed.paragraph_space_before = value,
|
||||
ParaNumber::SpaceAfter => ed.paragraph_space_after = value,
|
||||
}
|
||||
let para0 = doc_para0(&ed.doc);
|
||||
let cells = doc_to_cells(&ed.doc);
|
||||
let (start, end) = ed
|
||||
.sel
|
||||
.filter(|(start, end)| start < end)
|
||||
.unwrap_or((ed.caret, ed.caret));
|
||||
let paragraph_of = |index: usize| {
|
||||
cells
|
||||
.iter()
|
||||
.take(index.min(cells.len()))
|
||||
.filter(|cell| matches!(cell, Cell::Break(..)))
|
||||
.count()
|
||||
};
|
||||
let first = paragraph_of(start);
|
||||
let last = paragraph_of(end.saturating_sub((end > start) as usize));
|
||||
let mut doc = cells_to_doc(¶0, &cells);
|
||||
for paragraph in doc
|
||||
.paragraphs
|
||||
.iter_mut()
|
||||
.skip(first)
|
||||
.take(last.saturating_sub(first) + 1)
|
||||
{
|
||||
let optional = (number != 0.0).then_some(number);
|
||||
match field {
|
||||
ParaNumber::FirstIndent => paragraph.properties.first_line_indent = optional,
|
||||
ParaNumber::LeftIndent => paragraph.properties.left_margin = optional,
|
||||
ParaNumber::RightIndent => paragraph.properties.right_margin = optional,
|
||||
ParaNumber::SpaceBefore => paragraph.properties.spacing_before = optional,
|
||||
ParaNumber::SpaceAfter => paragraph.properties.spacing_after = optional,
|
||||
}
|
||||
}
|
||||
ed.content = text_editor::Content::with_text(&doc.to_mtext_string());
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
pub(super) fn mtext_stack_selection(&mut self) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
let Some((start, end)) = ed.sel.filter(|(start, end)| start < end) else {
|
||||
return;
|
||||
};
|
||||
let para0 = doc_para0(&ed.doc);
|
||||
let mut cells = doc_to_cells(&ed.doc);
|
||||
let end = end.min(cells.len());
|
||||
let plain = cells_to_doc(&ParagraphProperties::default(), &cells[start..end])
|
||||
.to_plain_text();
|
||||
let Some((split, separator)) = plain
|
||||
.char_indices()
|
||||
.find(|(_, ch)| matches!(ch, '/' | '#' | '^'))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let separator_len = separator.len_utf8();
|
||||
let data = StackingData {
|
||||
numerator: plain[..split].to_string(),
|
||||
denominator: plain[split + separator_len..].to_string(),
|
||||
stacking_type: StackingType::from_char(separator),
|
||||
};
|
||||
if data.numerator.is_empty() || data.denominator.is_empty() {
|
||||
return;
|
||||
}
|
||||
let props = cells[start..end]
|
||||
.iter()
|
||||
.find_map(|cell| match cell {
|
||||
Cell::Char(_, props) | Cell::Stack { props, .. } => Some(props.clone()),
|
||||
Cell::Break(..) => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
ed.record_undo();
|
||||
let replacement = flatten_stack(&data)
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(index, _)| Cell::Stack {
|
||||
data: data.clone(),
|
||||
props: props.clone(),
|
||||
head: index == 0,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let replacement_len = replacement.len();
|
||||
cells.splice(start..end, replacement);
|
||||
let new_end = start + replacement_len;
|
||||
ed.content = text_editor::Content::with_text(
|
||||
&cells_to_doc(¶0, &cells).to_mtext_string(),
|
||||
);
|
||||
ed.sel = Some((start, new_end));
|
||||
ed.caret = new_end;
|
||||
ed.sel_anchor = start;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
pub(super) fn mtext_clear_formatting(&mut self) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
let para0 = doc_para0(&ed.doc);
|
||||
let mut cells = doc_to_cells(&ed.doc);
|
||||
let (start, end) = ed
|
||||
.sel
|
||||
.filter(|(start, end)| start < end)
|
||||
.unwrap_or((0, cells.len()));
|
||||
let end = end.min(cells.len());
|
||||
ed.record_undo();
|
||||
for cell in &mut cells[start.min(end)..end] {
|
||||
if let Cell::Char(_, props) | Cell::Stack { props, .. } = cell {
|
||||
*props = SpanProperties::default();
|
||||
}
|
||||
}
|
||||
ed.content = text_editor::Content::with_text(
|
||||
&cells_to_doc(¶0, &cells).to_mtext_string(),
|
||||
);
|
||||
ed.sel = Some((start, end));
|
||||
ed.caret = end;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
pub(super) fn mtext_find_next(&mut self) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
let needle: Vec<char> = ed.find_text.chars().collect();
|
||||
if needle.is_empty() {
|
||||
return;
|
||||
}
|
||||
let cells = doc_to_cells(&ed.doc);
|
||||
let chars: Vec<Option<char>> = cells
|
||||
.iter()
|
||||
.map(|cell| match cell {
|
||||
Cell::Char(ch, _) => Some(*ch),
|
||||
Cell::Break(..) => Some('\n'),
|
||||
Cell::Stack { .. } => None,
|
||||
})
|
||||
.collect();
|
||||
if needle.len() > chars.len() {
|
||||
return;
|
||||
}
|
||||
let start = ed.caret.min(chars.len());
|
||||
let last_start = chars.len() - needle.len();
|
||||
let found = (start..=last_start)
|
||||
.chain(0..start.min(last_start + 1))
|
||||
.find(|index| {
|
||||
chars[*index..*index + needle.len()]
|
||||
.iter()
|
||||
.zip(&needle)
|
||||
.all(|(actual, expected)| actual == &Some(*expected))
|
||||
});
|
||||
if let Some(found) = found {
|
||||
ed.sel_anchor = found;
|
||||
ed.sel = Some((found, found + needle.len()));
|
||||
ed.caret = found + needle.len();
|
||||
ed.caret_blink_on = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn mtext_replace_next(&mut self) {
|
||||
let replacement = self
|
||||
.mtext_editor
|
||||
.as_ref()
|
||||
.map(|editor| editor.replace_text.clone())
|
||||
.unwrap_or_default();
|
||||
let matches = self.mtext_editor.as_ref().is_some_and(|editor| {
|
||||
let Some((start, end)) = editor.sel.filter(|(start, end)| start < end) else {
|
||||
return false;
|
||||
};
|
||||
let cells = doc_to_cells(&editor.doc);
|
||||
cells_to_doc(
|
||||
&ParagraphProperties::default(),
|
||||
&cells[start.min(cells.len())..end.min(cells.len())],
|
||||
)
|
||||
.to_plain_text()
|
||||
== editor.find_text
|
||||
});
|
||||
if matches {
|
||||
self.mtext_type(&replacement);
|
||||
}
|
||||
self.mtext_find_next();
|
||||
}
|
||||
|
||||
pub(super) fn mtext_replace_all(&mut self) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
let needle: Vec<char> = ed.find_text.chars().collect();
|
||||
if needle.is_empty() {
|
||||
return;
|
||||
}
|
||||
let para0 = doc_para0(&ed.doc);
|
||||
let mut cells = doc_to_cells(&ed.doc);
|
||||
let replacement = ed.replace_text.clone();
|
||||
let mut matches = Vec::new();
|
||||
let mut index = 0usize;
|
||||
while index + needle.len() <= cells.len() {
|
||||
let found = cells[index..index + needle.len()]
|
||||
.iter()
|
||||
.zip(&needle)
|
||||
.all(|(cell, expected)| matches!(cell, Cell::Char(actual, _) if actual == expected));
|
||||
if found {
|
||||
matches.push(index);
|
||||
index += needle.len();
|
||||
} else {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
if matches.is_empty() {
|
||||
return;
|
||||
}
|
||||
ed.record_undo();
|
||||
for start in matches.into_iter().rev() {
|
||||
let props = insert_props(&cells, start);
|
||||
let paragraph = para_props_at(¶0, &cells, start);
|
||||
let replacement_cells = str_to_cells(&replacement, &props, ¶graph);
|
||||
cells.splice(start..start + needle.len(), replacement_cells);
|
||||
}
|
||||
ed.content = text_editor::Content::with_text(
|
||||
&cells_to_doc(¶0, &cells).to_mtext_string(),
|
||||
);
|
||||
ed.caret = cells.len();
|
||||
ed.sel = Some((ed.caret, ed.caret));
|
||||
ed.sel_anchor = ed.caret;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
/// Toggle a character format over the preview selection, on the structured
|
||||
/// span properties. The stroke-font renderer has no true bold, so Bold
|
||||
/// switches the run to the heavier "Gothic" face; Italic applies a 15°
|
||||
@@ -798,6 +1218,7 @@ impl super::OpenCADStudio {
|
||||
if a >= b {
|
||||
return;
|
||||
}
|
||||
ed.record_undo();
|
||||
match kind {
|
||||
MTextFmt::Underline => {
|
||||
let on = !all_have(&cells[a..b], |p| p.underline());
|
||||
@@ -882,6 +1303,7 @@ impl super::OpenCADStudio {
|
||||
/// Set the alignment of every paragraph the selection (or caret) touches.
|
||||
pub(super) fn mtext_apply_align(&mut self, align: ParaAlign) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.record_undo();
|
||||
let para0 = doc_para0(&ed.doc);
|
||||
let cells = doc_to_cells(&ed.doc);
|
||||
let (a, b) = ed
|
||||
@@ -933,6 +1355,7 @@ impl super::OpenCADStudio {
|
||||
s
|
||||
};
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.record_undo();
|
||||
// Edit the structured document as a flat cell list, then serialize
|
||||
// it back into the raw content the preview/commit still read.
|
||||
let para0 = doc_para0(&ed.doc);
|
||||
@@ -962,6 +1385,7 @@ impl super::OpenCADStudio {
|
||||
/// Delete the selection, or the visible character before the caret.
|
||||
pub(super) fn mtext_backspace(&mut self) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.record_undo();
|
||||
let para0 = doc_para0(&ed.doc);
|
||||
let mut cells = doc_to_cells(&ed.doc);
|
||||
let count = cells.len();
|
||||
@@ -985,6 +1409,7 @@ impl super::OpenCADStudio {
|
||||
/// Delete the selection, or the visible character at the caret.
|
||||
pub(super) fn mtext_delete(&mut self) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.record_undo();
|
||||
let para0 = doc_para0(&ed.doc);
|
||||
let mut cells = doc_to_cells(&ed.doc);
|
||||
let count = cells.len();
|
||||
@@ -1104,6 +1529,7 @@ impl super::OpenCADStudio {
|
||||
let mut cells = doc_to_cells(&ed.doc);
|
||||
let b = b.min(cells.len());
|
||||
if a < b {
|
||||
ed.record_undo();
|
||||
for c in &mut cells[a..b] {
|
||||
if let Cell::Char(_, p) | Cell::Stack { props: p, .. } = c {
|
||||
let (bold, italic) = p
|
||||
@@ -1155,6 +1581,7 @@ impl super::OpenCADStudio {
|
||||
let mut cells = doc_to_cells(&ed.doc);
|
||||
let b = b.min(cells.len());
|
||||
if a < b {
|
||||
ed.record_undo();
|
||||
for c in &mut cells[a..b] {
|
||||
if let Cell::Char(_, p) | Cell::Stack { props: p, .. } = c {
|
||||
p.color = mcolor.clone();
|
||||
@@ -1217,7 +1644,8 @@ impl super::OpenCADStudio {
|
||||
}
|
||||
let body_empty = ed.content.text().trim().is_empty();
|
||||
let mut mt = ed.build_mtext();
|
||||
let annotative = ed.editing.is_none()
|
||||
let annotative = mt.is_annotative
|
||||
|| ed.editing.is_none()
|
||||
&& crate::scene::annotative::text_style_is_annotative(
|
||||
&self.tabs[i].scene.document,
|
||||
&mt.style,
|
||||
@@ -1240,9 +1668,12 @@ impl super::OpenCADStudio {
|
||||
Some(EntityType::MText(t)) => {
|
||||
t.value = mt.value;
|
||||
t.height = mt.height;
|
||||
t.style = mt.style;
|
||||
t.attachment_point = mt.attachment_point;
|
||||
t.line_spacing_factor = mt.line_spacing_factor;
|
||||
t.rectangle_width = mt.rectangle_width;
|
||||
t.rectangle_height = mt.rectangle_height;
|
||||
t.column_data = mt.column_data;
|
||||
}
|
||||
Some(EntityType::MultiLeader(ml)) => {
|
||||
ml.context.text_string = mt.value;
|
||||
@@ -1255,9 +1686,25 @@ impl super::OpenCADStudio {
|
||||
_ => {}
|
||||
}
|
||||
// Keep the displayed annotation context in sync.
|
||||
if matches!(self.tabs[i].scene.document.get_entity(h), Some(EntityType::MText(_))) {
|
||||
crate::scene::annotative::set_entity_annotative(
|
||||
&mut self.tabs[i].scene.document,
|
||||
h,
|
||||
annotative,
|
||||
);
|
||||
if annotative {
|
||||
if let Some(scale) = self.tabs[i].scene.current_annotation_scale_handle() {
|
||||
crate::scene::annotative::create_annotation_context(
|
||||
&mut self.tabs[i].scene.document,
|
||||
h,
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
self.tabs[i].scene.document.get_entity(h),
|
||||
Some(EntityType::MultiLeader(_))
|
||||
Some(EntityType::MText(_) | EntityType::MultiLeader(_))
|
||||
) {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
@@ -1283,7 +1730,6 @@ impl super::OpenCADStudio {
|
||||
position.y,
|
||||
position.z,
|
||||
);
|
||||
mt.rotation = 0.0;
|
||||
self.push_undo_snapshot(i, "MTEXT");
|
||||
let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt)));
|
||||
if annotative {
|
||||
@@ -1321,7 +1767,8 @@ impl super::OpenCADStudio {
|
||||
),
|
||||
None => return,
|
||||
};
|
||||
let annotative = editing.is_none()
|
||||
let annotative = mt.is_annotative
|
||||
|| editing.is_none()
|
||||
&& crate::scene::annotative::text_style_is_annotative(
|
||||
&self.tabs[i].scene.document,
|
||||
&mt.style,
|
||||
@@ -1341,9 +1788,12 @@ impl super::OpenCADStudio {
|
||||
Some(EntityType::MText(t)) => {
|
||||
t.value = mt.value;
|
||||
t.height = mt.height;
|
||||
t.style = mt.style;
|
||||
t.attachment_point = mt.attachment_point;
|
||||
t.line_spacing_factor = mt.line_spacing_factor;
|
||||
t.rectangle_width = mt.rectangle_width;
|
||||
t.rectangle_height = mt.rectangle_height;
|
||||
t.column_data = mt.column_data;
|
||||
}
|
||||
Some(EntityType::MultiLeader(ml)) => {
|
||||
ml.context.text_string = mt.value;
|
||||
@@ -1355,9 +1805,25 @@ impl super::OpenCADStudio {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if matches!(self.tabs[i].scene.document.get_entity(h), Some(EntityType::MText(_))) {
|
||||
crate::scene::annotative::set_entity_annotative(
|
||||
&mut self.tabs[i].scene.document,
|
||||
h,
|
||||
annotative,
|
||||
);
|
||||
if annotative {
|
||||
if let Some(scale) = self.tabs[i].scene.current_annotation_scale_handle() {
|
||||
crate::scene::annotative::create_annotation_context(
|
||||
&mut self.tabs[i].scene.document,
|
||||
h,
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
self.tabs[i].scene.document.get_entity(h),
|
||||
Some(EntityType::MultiLeader(_))
|
||||
Some(EntityType::MText(_) | EntityType::MultiLeader(_))
|
||||
) {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
@@ -1383,7 +1849,6 @@ impl super::OpenCADStudio {
|
||||
position.y,
|
||||
position.z,
|
||||
);
|
||||
mt.rotation = 0.0;
|
||||
self.push_undo_snapshot(i, "MTEXT");
|
||||
let handle = self.commit_entity_handle(plane.place_entity(EntityType::MText(mt)));
|
||||
self.tabs[i].dirty = true;
|
||||
@@ -1400,6 +1865,11 @@ impl super::OpenCADStudio {
|
||||
// Bind the editor to the fresh entity so the next Apply updates it.
|
||||
if let (Some(h), Some(ed)) = (handle, self.mtext_editor.as_mut()) {
|
||||
ed.editing = Some(h);
|
||||
if let Some(EntityType::MText(mtext)) =
|
||||
self.tabs[i].scene.document.get_entity(h)
|
||||
{
|
||||
ed.original = Some(mtext.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.refresh_properties();
|
||||
|
||||
+159
-8
@@ -2,6 +2,7 @@ use super::helpers::{entity_type_key, entity_type_label, title_case_word};
|
||||
use super::{OpenCADStudio, VARIES_LABEL};
|
||||
use crate::io::linetypes;
|
||||
use crate::scene::view::dispatch;
|
||||
use crate::scene::model::object::PropValue;
|
||||
use crate::ui;
|
||||
use crate::t;
|
||||
use acadrust::types::{Transform, Vector3};
|
||||
@@ -1310,11 +1311,88 @@ impl OpenCADStudio {
|
||||
// Feature-control frame: FCF text style is the dimension
|
||||
// style's DIMTXSTY.
|
||||
acadrust::EntityType::Tolerance(tol) => {
|
||||
if let Some(ds) = find_dim_style(doc, &tol.dimension_style_name) {
|
||||
if !ds.dimtxsty.is_empty() {
|
||||
set_row(&mut sections, "tol_text_style", ds.dimtxsty.clone());
|
||||
}
|
||||
use crate::entities::dim_override as dov;
|
||||
let style = crate::entities::tolerance::resolve_dim_style(tol, doc);
|
||||
let style_name = style
|
||||
.map(|entry| entry.name.clone())
|
||||
.unwrap_or_else(|| {
|
||||
if tol.dimension_style_name.trim().is_empty() {
|
||||
"Standard".to_string()
|
||||
} else {
|
||||
tol.dimension_style_name.clone()
|
||||
}
|
||||
});
|
||||
let mut dim_style_names: Vec<String> = doc
|
||||
.dim_styles
|
||||
.iter()
|
||||
.map(|entry| entry.name.clone())
|
||||
.filter(|name| !name.trim().is_empty())
|
||||
.collect();
|
||||
if !dim_style_names
|
||||
.iter()
|
||||
.any(|name| name.eq_ignore_ascii_case(&style_name))
|
||||
{
|
||||
dim_style_names.push(style_name.clone());
|
||||
}
|
||||
set_row_value(
|
||||
&mut sections,
|
||||
"tol_dim_style",
|
||||
PropValue::Choice {
|
||||
selected: style_name,
|
||||
options: dim_style_names,
|
||||
},
|
||||
);
|
||||
|
||||
let text_style_name = dov::handle(
|
||||
&tol.common.extended_data,
|
||||
dov::DIMTXSTY,
|
||||
)
|
||||
.and_then(|handle| {
|
||||
doc.text_styles
|
||||
.iter()
|
||||
.find(|entry| entry.handle == handle)
|
||||
})
|
||||
.map(|entry| entry.name.clone())
|
||||
.or_else(|| style.map(|entry| entry.dimtxsty.clone()))
|
||||
.unwrap_or_else(|| "Standard".to_string());
|
||||
let text_height_editable = doc
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|entry| {
|
||||
entry.name.eq_ignore_ascii_case(&text_style_name)
|
||||
})
|
||||
.is_none_or(|entry| !entry.has_fixed_height());
|
||||
let mut names = text_style_names.clone();
|
||||
if !names
|
||||
.iter()
|
||||
.any(|name| name.eq_ignore_ascii_case(&text_style_name))
|
||||
{
|
||||
names.push(text_style_name.clone());
|
||||
}
|
||||
set_row_value(
|
||||
&mut sections,
|
||||
"tol_text_style",
|
||||
PropValue::Choice {
|
||||
selected: text_style_name,
|
||||
options: names,
|
||||
},
|
||||
);
|
||||
|
||||
let height = dov::real(
|
||||
&tol.common.extended_data,
|
||||
dov::DIMTXT,
|
||||
)
|
||||
.or_else(|| style.map(|entry| entry.dimtxt))
|
||||
.unwrap_or(tol.text_height);
|
||||
set_row_value(
|
||||
&mut sections,
|
||||
"tol_text_height",
|
||||
if text_height_editable {
|
||||
PropValue::EditText(format!("{height:.4}"))
|
||||
} else {
|
||||
PropValue::ReadOnly(format!("{height:.4}"))
|
||||
},
|
||||
);
|
||||
}
|
||||
// MultiLeader: max points + segment-angle constraints
|
||||
// are MLeaderStyle settings, not stored on the entity.
|
||||
@@ -1705,7 +1783,7 @@ impl OpenCADStudio {
|
||||
{
|
||||
// Which entities show an Annotative row, the field it uses,
|
||||
// and — for those that don't already carry the row
|
||||
// (dimension / table) — the existing field to insert it
|
||||
// (dimension / table / tolerance) — the existing field to insert it
|
||||
// after. MLeader uses its editable toggle field.
|
||||
let anno: Option<(&str, Option<&str>)> = match entity {
|
||||
acadrust::EntityType::Text(_)
|
||||
@@ -1719,6 +1797,9 @@ impl OpenCADStudio {
|
||||
acadrust::EntityType::Dimension(_) => {
|
||||
Some(("annotative", Some("style_name")))
|
||||
}
|
||||
acadrust::EntityType::Tolerance(_) => {
|
||||
Some(("annotative", Some("tol_dim_style")))
|
||||
}
|
||||
acadrust::EntityType::Table(_) => {
|
||||
Some(("annotative", Some("tbl_style_handle")))
|
||||
}
|
||||
@@ -1727,15 +1808,22 @@ impl OpenCADStudio {
|
||||
if let Some((anno_field, insert_after)) = anno {
|
||||
let is_anno = crate::scene::annotative::is_annotative(doc, entity)
|
||||
|| match entity {
|
||||
acadrust::EntityType::Dimension(dimension) => {
|
||||
acadrust::EntityType::Dimension(
|
||||
acadrust::entities::Dimension::Arc(dimension),
|
||||
) => {
|
||||
crate::scene::annotative::dim_style_is_annotative(
|
||||
doc,
|
||||
&dimension.base().style_name,
|
||||
&dimension.base.style_name,
|
||||
)
|
||||
}
|
||||
acadrust::EntityType::Tolerance(_) => {
|
||||
crate::scene::annotative::annotation_style_is_annotative(
|
||||
doc, entity,
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
// Dimensions/tables carry no Annotative row yet — add one
|
||||
// Dimensions/tables/tolerances carry no Annotative row yet — add one
|
||||
// right after their style row.
|
||||
if let Some(anchor) = insert_after {
|
||||
insert_row_after(
|
||||
@@ -1815,6 +1903,69 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// Single-line text height rows depend on both the
|
||||
// justification and the active annotation scale. Aligned
|
||||
// text derives its paper height from the two endpoints,
|
||||
// while annotative text exposes that paper height and a
|
||||
// separate calculated model height.
|
||||
if let acadrust::EntityType::Text(text) = entity {
|
||||
let aligned = matches!(
|
||||
text.horizontal_alignment,
|
||||
acadrust::entities::TextHorizontalAlignment::Aligned
|
||||
);
|
||||
let annotative = crate::scene::annotative::is_annotative(doc, entity);
|
||||
let model_factor = if annotative {
|
||||
annotation_scale_handle
|
||||
.and_then(|handle| match doc.objects.get(&handle) {
|
||||
Some(acadrust::objects::ObjectType::Scale(scale)) => Some(
|
||||
scale.inverse_factor()
|
||||
/ self.tabs[i].scene.annotation_scale_unit_factor(),
|
||||
),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(self.tabs[i].scene.annotation_scale as f64)
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let paper_height = if aligned {
|
||||
crate::entities::text::text_run_placement_at_scale(
|
||||
text,
|
||||
doc,
|
||||
model_factor as f32,
|
||||
)
|
||||
.height as f64
|
||||
} else {
|
||||
text.height
|
||||
};
|
||||
for section in sections.iter_mut() {
|
||||
if let Some(row) =
|
||||
section.props.iter_mut().find(|row| row.field == "height")
|
||||
{
|
||||
if annotative {
|
||||
row.label = t!("Paper text height").into_owned();
|
||||
}
|
||||
if aligned {
|
||||
row.value = crate::scene::model::object::PropValue::ReadOnly(
|
||||
crate::entities::common::format_length(paper_height),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if annotative {
|
||||
insert_row_after(
|
||||
&mut sections,
|
||||
"height",
|
||||
crate::entities::common::ro_prop(
|
||||
t!("Model text height").as_ref(),
|
||||
"model_text_height",
|
||||
crate::entities::common::format_length(
|
||||
paper_height * model_factor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !group_names.is_empty() {
|
||||
let label = group_names.join(", ");
|
||||
if let Some(general) = sections.first_mut() {
|
||||
|
||||
+66
-24
@@ -95,6 +95,9 @@ pub struct TextInlineState {
|
||||
pub editing: Option<Handle>,
|
||||
/// Which entity slot this session writes to on commit.
|
||||
pub field: TextEntityField,
|
||||
/// Fully prepared entity supplied by the interactive TEXT command. Editing
|
||||
/// existing text and legacy direct-open paths leave this empty.
|
||||
pub creation: Option<Text>,
|
||||
/// Canvas-space anchor where the field is drawn (the insertion-point click).
|
||||
pub screen_anchor: iced::Point,
|
||||
}
|
||||
@@ -145,6 +148,13 @@ impl super::OpenCADStudio {
|
||||
if self.tabs[i].scene.is_layer_locked(target) {
|
||||
return iced::Task::none();
|
||||
}
|
||||
if matches!(
|
||||
self.tabs[i].scene.document.get_entity(target),
|
||||
Some(EntityType::Tolerance(_))
|
||||
) {
|
||||
self.open_tolerance_dialog(Some(target));
|
||||
return iced::Task::none();
|
||||
}
|
||||
// Snapshot what we need before borrowing `self` mutably to open.
|
||||
let Some(entity) = self.tabs[i].scene.document.get_entity(target) else {
|
||||
return iced::Task::none();
|
||||
@@ -166,10 +176,10 @@ impl super::OpenCADStudio {
|
||||
};
|
||||
|
||||
if field.is_rich() {
|
||||
self.open_mtext_editor(pos, Some(target), &value, height);
|
||||
self.open_mtext_editor(pos, Some(target), &value, height, None);
|
||||
self.unfocus_widgets()
|
||||
} else {
|
||||
self.open_text_inline(pos, Some(target), &value, height, field);
|
||||
self.open_text_inline(pos, Some(target), &value, height, field, None);
|
||||
iced::widget::operation::focus(iced::widget::Id::new(super::view::TEXT_INLINE_ID))
|
||||
}
|
||||
}
|
||||
@@ -183,6 +193,7 @@ impl super::OpenCADStudio {
|
||||
initial: &str,
|
||||
height: f64,
|
||||
field: TextEntityField,
|
||||
creation: Option<Text>,
|
||||
) {
|
||||
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
|
||||
return;
|
||||
@@ -193,6 +204,7 @@ impl super::OpenCADStudio {
|
||||
height: if height > 0.0 { height } else { 0.25 },
|
||||
editing: handle,
|
||||
field,
|
||||
creation,
|
||||
screen_anchor: iced::Point::new(60.0, 90.0),
|
||||
};
|
||||
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos {
|
||||
@@ -229,12 +241,18 @@ impl super::OpenCADStudio {
|
||||
} else {
|
||||
crate::command::WorkingPlane::default()
|
||||
};
|
||||
let position = plane.to_local(ed.pos);
|
||||
let mut t = Text::with_value(
|
||||
&ed.value,
|
||||
Vector3::new(position.x, position.y, position.z),
|
||||
)
|
||||
.with_height(ed.height);
|
||||
let command_creation = ed.creation.is_some();
|
||||
let mut t = if let Some(mut prepared) = ed.creation {
|
||||
prepared.value = ed.value.clone();
|
||||
prepared
|
||||
} else {
|
||||
let position = plane.to_local(ed.pos);
|
||||
Text::with_value(
|
||||
&ed.value,
|
||||
Vector3::new(position.x, position.y, position.z),
|
||||
)
|
||||
.with_height(ed.height)
|
||||
};
|
||||
// New text inherits the document's current text style (STYLE), not
|
||||
// the entity default. See #92.
|
||||
let cur_style = self.tabs[i]
|
||||
@@ -244,24 +262,48 @@ impl super::OpenCADStudio {
|
||||
.current_text_style_name
|
||||
.clone();
|
||||
if !cur_style.is_empty() {
|
||||
t.style = cur_style;
|
||||
}
|
||||
let annotative = crate::scene::annotative::text_style_is_annotative(
|
||||
&self.tabs[i].scene.document,
|
||||
&t.style,
|
||||
);
|
||||
self.push_undo_snapshot(i, "TEXT");
|
||||
let handle = self.commit_entity_handle(plane.place_entity(EntityType::Text(t)));
|
||||
if annotative {
|
||||
let scale = self.tabs[i].scene.current_annotation_scale_handle();
|
||||
if let (Some(handle), Some(scale)) = (handle, scale) {
|
||||
crate::scene::annotative::create_annotation_context(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
scale,
|
||||
);
|
||||
if t.style.trim().is_empty() {
|
||||
t.style = cur_style;
|
||||
}
|
||||
}
|
||||
if command_creation {
|
||||
let annotation_multiplier = if crate::scene::annotative::text_style_is_annotative(
|
||||
&self.tabs[i].scene.document,
|
||||
&t.style,
|
||||
) {
|
||||
self.tabs[i].scene.creation_annotation_multiplier()
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let display_height = crate::entities::text::text_run_placement_at_scale(
|
||||
&t,
|
||||
&self.tabs[i].scene.document,
|
||||
annotation_multiplier as f32,
|
||||
)
|
||||
.height as f64
|
||||
* annotation_multiplier;
|
||||
if let Some(command) = self.tabs[i].suspended_cmd.as_mut() {
|
||||
command.on_editor_display_height(display_height);
|
||||
}
|
||||
}
|
||||
self.push_undo_snapshot(i, "TEXT");
|
||||
self.tabs[i].scene.document.header.current_text_style_name = t.style.clone();
|
||||
let variable_height = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|style| style.name.eq_ignore_ascii_case(&t.style))
|
||||
.is_none_or(|style| style.height <= 1.0e-9);
|
||||
if variable_height
|
||||
&& !matches!(
|
||||
t.horizontal_alignment,
|
||||
acadrust::entities::TextHorizontalAlignment::Aligned
|
||||
)
|
||||
{
|
||||
self.tabs[i].scene.document.header.text_height = t.height;
|
||||
}
|
||||
let _ = self.commit_entity_handle(plane.place_entity(EntityType::Text(t)));
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
self.refresh_properties();
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
use acadrust::{EntityType, Handle};
|
||||
|
||||
impl super::OpenCADStudio {
|
||||
pub(super) fn open_tolerance_dialog(&mut self, editing: Option<Handle>) {
|
||||
let i = self.active_tab;
|
||||
if editing.is_some_and(|handle| self.tabs[i].scene.is_layer_locked(handle)) {
|
||||
return;
|
||||
}
|
||||
let text = editing
|
||||
.and_then(|handle| self.tabs[i].scene.document.get_entity(handle))
|
||||
.and_then(|entity| match entity {
|
||||
EntityType::Tolerance(tolerance) => Some(tolerance.text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
self.geometric_tolerance = Some(
|
||||
crate::ui::window::geometric_tolerance::State::from_text(editing, text),
|
||||
);
|
||||
self.active_modal = Some(super::ModalKind::GeometricTolerance);
|
||||
self.modal_offset = iced::Vector::ZERO;
|
||||
self.modal_resize = iced::Vector::ZERO;
|
||||
self.modal_content_size = None;
|
||||
self.modal_drag_last = None;
|
||||
self.modal_dragging = false;
|
||||
self.modal_resizing = false;
|
||||
}
|
||||
|
||||
pub(super) fn apply_tolerance_dialog_edit(&mut self) -> bool {
|
||||
let Some(state) = self.geometric_tolerance.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let Some(handle) = state.editing else {
|
||||
return false;
|
||||
};
|
||||
if !state.is_valid() || self.tabs[self.active_tab].scene.is_layer_locked(handle) {
|
||||
return false;
|
||||
}
|
||||
let text = state.to_text();
|
||||
let i = self.active_tab;
|
||||
let unchanged = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.is_some_and(|entity| {
|
||||
matches!(entity, EntityType::Tolerance(tolerance) if tolerance.text == text)
|
||||
});
|
||||
if unchanged {
|
||||
return true;
|
||||
}
|
||||
self.push_undo_snapshot(i, "TOLERANCE");
|
||||
let Some(EntityType::Tolerance(tolerance)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
tolerance.text = text;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn begin_tolerance_placement(&mut self) -> bool {
|
||||
let Some(state) = self.geometric_tolerance.take() else {
|
||||
return false;
|
||||
};
|
||||
if !state.is_valid() || state.editing.is_some() {
|
||||
self.geometric_tolerance = Some(state);
|
||||
return false;
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let text = state.to_text();
|
||||
let mut preview_entity = EntityType::Tolerance(
|
||||
acadrust::entities::Tolerance::with_text(
|
||||
acadrust::types::Vector3::ZERO,
|
||||
text.clone(),
|
||||
),
|
||||
);
|
||||
crate::scene::creation_style::apply_current_creation_styles(
|
||||
&self.tabs[i].scene.document,
|
||||
&mut preview_entity,
|
||||
);
|
||||
let preview_strokes = match &preview_entity {
|
||||
EntityType::Tolerance(tolerance) => crate::entities::tolerance::preview_strokes(
|
||||
tolerance,
|
||||
&self.tabs[i].scene.document,
|
||||
),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let mut command = crate::modules::annotate::tolerance_cmd::ToleranceCommand::with_text(
|
||||
text,
|
||||
preview_strokes,
|
||||
);
|
||||
use crate::command::CadCommand;
|
||||
let plane = if self.tabs[i].editing_model_space() {
|
||||
self.tabs[i].ucs_xform().working_plane()
|
||||
} else {
|
||||
crate::command::WorkingPlane::default()
|
||||
};
|
||||
command.set_working_plane(plane);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
self.active_modal = None;
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -1162,6 +1162,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
if matches!(
|
||||
item.action,
|
||||
GripMenuAction::Stretch
|
||||
| GripMenuAction::MoveWithText
|
||||
| GripMenuAction::MoveWithDimLine
|
||||
| GripMenuAction::MoveWithLeader
|
||||
| GripMenuAction::MoveIndependent
|
||||
@@ -1212,7 +1213,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
{
|
||||
(crate::entities::multileader::MOVE_ALL_GRIP, true)
|
||||
} else {
|
||||
(popup.grip_id, if is_dimension { false } else { g.is_midpoint })
|
||||
(
|
||||
popup.grip_id,
|
||||
matches!(item.action, GripMenuAction::MoveWithText)
|
||||
|| (!is_dimension && g.is_midpoint),
|
||||
)
|
||||
};
|
||||
self.tabs[i].active_grip = Some(GripEdit::single(
|
||||
popup.handle,
|
||||
@@ -1901,7 +1906,73 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
self.push_undo_snapshot(i, "CHPROP");
|
||||
|
||||
if field.starts_with("dim_") {
|
||||
if field == "tol_text_style" {
|
||||
use crate::entities::dim_override as dov;
|
||||
use acadrust::xdata::XDataValue;
|
||||
let style_handle = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case(&value))
|
||||
.map(|entry| entry.handle);
|
||||
if let Some(style_handle) = style_handle {
|
||||
for &handle in &handles {
|
||||
if self.tabs[i].scene.is_layer_locked(handle)
|
||||
|| !matches!(
|
||||
self.tabs[i].scene.document.get_entity(handle),
|
||||
Some(acadrust::EntityType::Tolerance(_))
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
dov::set(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
dov::DIMTXSTY,
|
||||
Some(XDataValue::Handle(style_handle)),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if field == "tol_dim_style" {
|
||||
let style = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.find(|entry| entry.name.eq_ignore_ascii_case(&value))
|
||||
.map(|entry| (entry.handle, entry.name.clone(), entry.annotative));
|
||||
if let Some((style_handle, style_name, annotative)) = style {
|
||||
let scale = self.tabs[i].scene.creation_annotation_scale_handle();
|
||||
for &handle in &handles {
|
||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||
continue;
|
||||
}
|
||||
if let Some(acadrust::EntityType::Tolerance(tolerance)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
tolerance.dimension_style_handle = Some(style_handle);
|
||||
tolerance.dimension_style_name = style_name.clone();
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
crate::scene::annotative::set_entity_annotative(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
annotative,
|
||||
);
|
||||
if annotative {
|
||||
if let Some(scale) = scale {
|
||||
crate::scene::annotative::create_annotation_context(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if field.starts_with("dim_") {
|
||||
for &handle in &handles {
|
||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||
continue;
|
||||
@@ -2552,6 +2623,27 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
continue;
|
||||
}
|
||||
match field {
|
||||
"tol_text_height" => {
|
||||
use crate::entities::dim_override as dov;
|
||||
let trimmed = val.trim();
|
||||
if trimmed.is_empty() {
|
||||
dov::set(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
dov::DIMTXT,
|
||||
None,
|
||||
);
|
||||
} else if let Ok(height) = trimmed.parse::<f64>() {
|
||||
if height > 0.0 {
|
||||
dov::set(
|
||||
&mut self.tabs[i].scene.document,
|
||||
handle,
|
||||
dov::DIMTXT,
|
||||
Some(acadrust::xdata::XDataValue::Real(height)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ if field.starts_with("dim_") => {
|
||||
if matches!(
|
||||
self.tabs[i].scene.document.get_entity(handle),
|
||||
|
||||
+169
-11
@@ -195,6 +195,7 @@ impl OpenCADStudio {
|
||||
self.attr_editor_selected = 0;
|
||||
self.attr_editor_tab = crate::ui::window::attribute_editor::AttrTab::Attribute;
|
||||
}
|
||||
Some(GeometricTolerance) => self.geometric_tolerance = None,
|
||||
// Closing (✕) discards edits made since the last Apply — matching the
|
||||
// style editors. Committing happens only through the Apply button.
|
||||
Some(Aliases) => self.alias_editor_rows.clear(),
|
||||
@@ -234,6 +235,25 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit `SelectionChangedV4` to V4 plugins when the active tab's selection
|
||||
/// set actually changed since the last broadcast.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn notify_plugins_selection_changed(&mut self) {
|
||||
if self.active_tab >= self.tabs.len() {
|
||||
return;
|
||||
}
|
||||
let i = self.active_tab;
|
||||
let tab_id = self.tabs[i].id;
|
||||
let fingerprint = self.tabs[i].scene.selection_fingerprint();
|
||||
let key = (tab_id, fingerprint);
|
||||
if self.last_plugin_selection == Some(key) {
|
||||
return;
|
||||
}
|
||||
self.last_plugin_selection = Some(key);
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
crate::plugin::v4_support::publish_selection_changed_v4(tab_id, handles);
|
||||
}
|
||||
|
||||
pub fn update(&mut self, msg: Message) -> Task<Message> {
|
||||
let perf_started = crate::perf::enabled().then(Instant::now);
|
||||
let perf_label = perf_message_label(&msg);
|
||||
@@ -278,6 +298,11 @@ impl OpenCADStudio {
|
||||
// The block panel watches the drawing's block list and rebuilds its
|
||||
// thumbnails whenever the names change (BLOCK define, file open, …).
|
||||
self.refresh_block_palette_if_stale();
|
||||
// Let V4 plugins observe selection changes that happened while handling
|
||||
// this message (picking, window select, QSELECT, SELECTALL, grip edits,
|
||||
// and plugin request draining).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
self.notify_plugins_selection_changed();
|
||||
// OTRACK acquires tracking points only while a command or grip drag is
|
||||
// running; drop them once neither is active so the temporary tracking
|
||||
// points / vectors disappear when the command ends (issue #64).
|
||||
@@ -3706,6 +3731,40 @@ impl OpenCADStudio {
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
Message::ToleranceDialogField(field) => {
|
||||
if let Some(state) = self.geometric_tolerance.as_mut() {
|
||||
state.apply_field(field);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ToleranceDialogToggle(toggle) => {
|
||||
if let Some(state) = self.geometric_tolerance.as_mut() {
|
||||
state.apply_toggle(toggle);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ToleranceDialogApply => {
|
||||
self.apply_tolerance_dialog_edit();
|
||||
Task::none()
|
||||
}
|
||||
Message::ToleranceDialogOk => {
|
||||
let editing = self
|
||||
.geometric_tolerance
|
||||
.as_ref()
|
||||
.and_then(|state| state.editing)
|
||||
.is_some();
|
||||
if editing {
|
||||
if self.apply_tolerance_dialog_edit() {
|
||||
self.geometric_tolerance = None;
|
||||
self.active_modal = None;
|
||||
self.reset_modal_geometry();
|
||||
}
|
||||
} else {
|
||||
self.begin_tolerance_placement();
|
||||
self.reset_modal_geometry();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::SetLinearFormat(code) => {
|
||||
self.units_popup_open = false;
|
||||
let i = self.active_tab;
|
||||
@@ -3880,10 +3939,7 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextHeight(s) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.height = s;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
self.mtext_apply_span_number("height", s);
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextRectWidth(width) => {
|
||||
@@ -3915,26 +3971,128 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextOblique(s) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.oblique = s;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
self.mtext_apply_span_number("oblique", s);
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextWidth(s) => {
|
||||
self.mtext_apply_span_number("width", s);
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextCharSpace(s) => {
|
||||
self.mtext_apply_span_number("tracking", s);
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextUndo => {
|
||||
self.mtext_undo();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextRedo => {
|
||||
self.mtext_redo();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextStack => {
|
||||
self.mtext_stack_selection();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextClearFormatting => {
|
||||
self.mtext_clear_formatting();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextInsert(value) => {
|
||||
self.mtext_type(&value);
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextAnnotative(value) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.width = s;
|
||||
ed.annotative = value;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextCharSpace(s) => {
|
||||
Message::MTextColumnMode(mode) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.char_space = s;
|
||||
match mode.as_str() {
|
||||
"Static" => {
|
||||
ed.column_type = 1;
|
||||
ed.column_auto_height = false;
|
||||
}
|
||||
"Dynamic auto" => {
|
||||
ed.column_type = 2;
|
||||
ed.column_auto_height = true;
|
||||
}
|
||||
"Dynamic manual" => {
|
||||
ed.column_type = 2;
|
||||
ed.column_auto_height = false;
|
||||
}
|
||||
_ => ed.column_type = 0,
|
||||
}
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextColumnCount(value) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.column_count = value;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextColumnWidth(value) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.column_width = value;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextColumnGutter(value) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.column_gutter = value;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextColumnHeight(value) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.rect_height = value;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextColumnFlowReversed(value) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.column_flow_reversed = value;
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextParagraphNumber(field, value) => {
|
||||
self.mtext_apply_paragraph_number(field, value);
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextFindText(value) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.find_text = value;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextReplaceText(value) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.replace_text = value;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextFindNext => {
|
||||
self.mtext_find_next();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextReplaceNext => {
|
||||
self.mtext_replace_next();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextReplaceAll => {
|
||||
self.mtext_replace_all();
|
||||
Task::none()
|
||||
}
|
||||
Message::MTextJustify(ap) => {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.attachment = ap;
|
||||
|
||||
@@ -21,6 +21,7 @@ impl OpenCADStudio {
|
||||
Some(K::LayerStateManager) => crate::tr!("modal", "layer-state-manager"),
|
||||
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
|
||||
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
|
||||
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
|
||||
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
|
||||
Some(K::LayerStateEditor) => crate::tr!("modal", "edit-layer-state"),
|
||||
Some(K::Plot) => crate::tr!("modal", "plot"),
|
||||
@@ -234,6 +235,12 @@ impl OpenCADStudio {
|
||||
crate::ui::window::drawing_units::view_window(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::GeometricTolerance => {
|
||||
let state = self.geometric_tolerance.as_ref()?;
|
||||
sized_flow(ex, 670, 530, |flow| {
|
||||
crate::ui::window::geometric_tolerance::view_window(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::LayerStateManager => {
|
||||
let states = self.tabs[self.active_tab].scene.document.layer_states();
|
||||
sized_flow(
|
||||
|
||||
+159
-6
@@ -6,7 +6,8 @@ use iced::advanced::renderer;
|
||||
use iced::advanced::widget;
|
||||
use iced::advanced::{Layout, Shell, Widget};
|
||||
use iced::widget::{
|
||||
button, column, container, mouse_area, row, scrollable, stack, text, text_input, Space,
|
||||
button, checkbox, column, container, mouse_area, row, scrollable, stack, text, text_input,
|
||||
Space,
|
||||
};
|
||||
use iced::{Background, Border, Color, Element, Event, Fill, Length, Rectangle, Size, Theme, Vector};
|
||||
use crate::t;
|
||||
@@ -374,7 +375,7 @@ pub(super) fn mtext_editor_overlay<'a>(
|
||||
iced::widget::Space::new().width(Fill).height(Fill),
|
||||
t!("Text Editor"),
|
||||
content,
|
||||
Message::MTextCancel,
|
||||
Message::MTextOk,
|
||||
modal_offset,
|
||||
crate::ui::modal::ModalOptions::STANDARD,
|
||||
)
|
||||
@@ -450,6 +451,9 @@ fn mtext_editor_content<'a>(
|
||||
.on_select(Message::MTextStyle)
|
||||
.text_size(11)
|
||||
.width(iced::Length::Fixed(96.0));
|
||||
let annotative = checkbox(ed.annotative)
|
||||
.on_toggle(Message::MTextAnnotative)
|
||||
.size(14);
|
||||
let font_sel = if ed.font.trim().is_empty() {
|
||||
"[Style default]".to_string()
|
||||
} else {
|
||||
@@ -487,6 +491,9 @@ fn mtext_editor_content<'a>(
|
||||
|
||||
let row1 = row![
|
||||
style_pl,
|
||||
row![annotative, text(t!("Annotative")).size(11)]
|
||||
.spacing(3)
|
||||
.align_y(iced::Alignment::Center),
|
||||
font_pl,
|
||||
small_input("2.5", &ed.height, Message::MTextHeight, 64.0),
|
||||
iced::widget::Space::new().width(6),
|
||||
@@ -535,6 +542,14 @@ fn mtext_editor_content<'a>(
|
||||
.text_size(11)
|
||||
.width(iced::Length::Fixed(112.0));
|
||||
let row2 = row![
|
||||
button(lbl("↶"))
|
||||
.on_press(Message::MTextUndo)
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
button(lbl("↷"))
|
||||
.on_press(Message::MTextRedo)
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
lbl("O"),
|
||||
small_input("0", &ed.oblique, Message::MTextOblique, 48.0),
|
||||
lbl("W"),
|
||||
@@ -573,6 +588,144 @@ fn mtext_editor_content<'a>(
|
||||
.on_press(Message::MTextLineSpacing(2.0))
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
button(lbl("Stack"))
|
||||
.on_press(Message::MTextStack)
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
button(lbl("Clear"))
|
||||
.on_press(Message::MTextClearFormatting)
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
button(lbl("°"))
|
||||
.on_press(Message::MTextInsert("°".to_string()))
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
button(lbl("±"))
|
||||
.on_press(Message::MTextInsert("±".to_string()))
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
button(lbl("⌀"))
|
||||
.on_press(Message::MTextInsert("⌀".to_string()))
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center)
|
||||
.width(width);
|
||||
|
||||
let column_mode = match (ed.column_type, ed.column_auto_height) {
|
||||
(1, _) => "Static",
|
||||
(2, true) => "Dynamic auto",
|
||||
(2, false) => "Dynamic manual",
|
||||
_ => "No columns",
|
||||
}
|
||||
.to_string();
|
||||
let column_picker = iced::widget::pick_list(
|
||||
Some(column_mode),
|
||||
["No columns", "Static", "Dynamic auto", "Dynamic manual"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>(),
|
||||
|value| value.to_string(),
|
||||
)
|
||||
.on_select(Message::MTextColumnMode)
|
||||
.text_size(11)
|
||||
.width(iced::Length::Fixed(120.0));
|
||||
let reverse = checkbox(ed.column_flow_reversed)
|
||||
.on_toggle(Message::MTextColumnFlowReversed)
|
||||
.size(14);
|
||||
let row3 = row![
|
||||
lbl("Columns"),
|
||||
column_picker,
|
||||
lbl("Count"),
|
||||
small_input("2", &ed.column_count, Message::MTextColumnCount, 42.0),
|
||||
lbl("Height"),
|
||||
text_input("0", &ed.rect_height)
|
||||
.on_input(Message::MTextColumnHeight)
|
||||
.width(iced::Length::Fixed(58.0))
|
||||
.padding(3)
|
||||
.size(12),
|
||||
lbl("Width"),
|
||||
small_input("0", &ed.column_width, Message::MTextColumnWidth, 58.0),
|
||||
lbl("Gutter"),
|
||||
small_input("0", &ed.column_gutter, Message::MTextColumnGutter, 58.0),
|
||||
reverse,
|
||||
lbl("Reverse"),
|
||||
iced::widget::Space::new().width(8),
|
||||
lbl("First"),
|
||||
text_input("0", &ed.paragraph_first_indent)
|
||||
.on_input(|value| Message::MTextParagraphNumber(
|
||||
super::super::mtext_editor::ParaNumber::FirstIndent,
|
||||
value,
|
||||
))
|
||||
.width(iced::Length::Fixed(48.0))
|
||||
.padding(3)
|
||||
.size(12),
|
||||
lbl("Left"),
|
||||
text_input("0", &ed.paragraph_left_indent)
|
||||
.on_input(|value| Message::MTextParagraphNumber(
|
||||
super::super::mtext_editor::ParaNumber::LeftIndent,
|
||||
value,
|
||||
))
|
||||
.width(iced::Length::Fixed(48.0))
|
||||
.padding(3)
|
||||
.size(12),
|
||||
lbl("Right"),
|
||||
text_input("0", &ed.paragraph_right_indent)
|
||||
.on_input(|value| Message::MTextParagraphNumber(
|
||||
super::super::mtext_editor::ParaNumber::RightIndent,
|
||||
value,
|
||||
))
|
||||
.width(iced::Length::Fixed(48.0))
|
||||
.padding(3)
|
||||
.size(12),
|
||||
lbl("Before"),
|
||||
text_input("0", &ed.paragraph_space_before)
|
||||
.on_input(|value| Message::MTextParagraphNumber(
|
||||
super::super::mtext_editor::ParaNumber::SpaceBefore,
|
||||
value,
|
||||
))
|
||||
.width(iced::Length::Fixed(48.0))
|
||||
.padding(3)
|
||||
.size(12),
|
||||
lbl("After"),
|
||||
text_input("0", &ed.paragraph_space_after)
|
||||
.on_input(|value| Message::MTextParagraphNumber(
|
||||
super::super::mtext_editor::ParaNumber::SpaceAfter,
|
||||
value,
|
||||
))
|
||||
.width(iced::Length::Fixed(48.0))
|
||||
.padding(3)
|
||||
.size(12),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center)
|
||||
.width(width);
|
||||
let row4 = row![
|
||||
lbl("Find"),
|
||||
text_input("Find", &ed.find_text)
|
||||
.on_input(Message::MTextFindText)
|
||||
.width(iced::Length::Fixed(140.0))
|
||||
.padding(3)
|
||||
.size(12),
|
||||
button(lbl("Next"))
|
||||
.on_press(Message::MTextFindNext)
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
lbl("Replace"),
|
||||
text_input("Replace", &ed.replace_text)
|
||||
.on_input(Message::MTextReplaceText)
|
||||
.width(iced::Length::Fixed(140.0))
|
||||
.padding(3)
|
||||
.size(12),
|
||||
button(lbl("Replace"))
|
||||
.on_press(Message::MTextReplaceNext)
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
button(lbl("Replace All"))
|
||||
.on_press(Message::MTextReplaceAll)
|
||||
.padding(3)
|
||||
.style(btn_style),
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center)
|
||||
@@ -710,13 +863,13 @@ fn mtext_editor_content<'a>(
|
||||
.into()
|
||||
};
|
||||
|
||||
// ── Top action bar: Apply on the right, exactly like the style managers'
|
||||
// toolbar strip. Closing (the modal ✕) without applying discards the
|
||||
// buffer, so there is no separate Cancel.
|
||||
// ── Top action bar: Apply keeps editing; Close saves and exits. Escape
|
||||
// remains the explicit discard path.
|
||||
let action_bar = container(
|
||||
row![
|
||||
iced::widget::Space::new().width(width),
|
||||
crate::ui::style::style_manager::tb_button(t!("Apply"), Message::MTextApply, true),
|
||||
crate::ui::style::style_manager::tb_button(t!("Close Text Editor"), Message::MTextOk, true),
|
||||
]
|
||||
.align_y(iced::Alignment::Center),
|
||||
)
|
||||
@@ -730,7 +883,7 @@ fn mtext_editor_content<'a>(
|
||||
.padding([5, 8]);
|
||||
|
||||
container(
|
||||
column![action_bar, row1, row2, width_slider, body]
|
||||
column![action_bar, row1, row2, row3, row4, width_slider, body]
|
||||
.spacing(6)
|
||||
.width(width)
|
||||
.height(height),
|
||||
|
||||
Reference in new issue
Block a user