From de294447a93e7a687b7bb112a5f2d179fc0852c8 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:54:20 +0300 Subject: [PATCH] Complete geometric tolerance workflow and properties --- src/app/commands/dim.rs | 5 +- src/app/mod.rs | 13 + src/app/properties.rs | 75 +++- src/app/text_inline.rs | 7 + src/app/tolerance_dialog.rs | 88 +++++ src/app/update/command.rs | 89 ++++- src/app/update/mod.rs | 35 ++ src/app/view/modal.rs | 7 + src/entities/tolerance.rs | 266 ++++++++++---- src/modules/annotate/tolerance_cmd.rs | 102 +++--- src/scene/cache/properties.rs | 4 +- src/scene/convert/tess.rs | 1 + src/ui/window/geometric_tolerance.rs | 479 ++++++++++++++++++++++++++ src/ui/window/mod.rs | 1 + 14 files changed, 1037 insertions(+), 135 deletions(-) create mode 100644 src/app/tolerance_dialog.rs create mode 100644 src/ui/window/geometric_tolerance.rs diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 0138c9fc2..31ef8651b 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -594,10 +594,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" => { diff --git a/src/app/mod.rs b/src/app/mod.rs index 8324cf55d..d9c5bc139 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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, /// Working copy of the Drawing Units dialog; `None` while it is closed. drawing_units: Option, + /// Working copy of the structured feature-control-frame editor. + geometric_tolerance: Option, /// PICKDRAG (#226): false (default) = press-drag lassoes; true = /// press-drag draws a rectangle marquee. pick_drag_rect: bool, @@ -1588,6 +1591,7 @@ pub enum ModalKind { LayerStateManager, LayerTranslator, DrawingUnits, + GeometricTolerance, DraftingSettings, LayerStateEditor, Plot, @@ -2318,6 +2322,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. @@ -3145,6 +3157,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, diff --git a/src/app/properties.rs b/src/app/properties.rs index bd7daa3c8..d4e51d939 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -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,77 @@ 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 = 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 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", + PropValue::EditText(format!("{height:.4}")), + ); } // MultiLeader: max points + segment-angle constraints // are MLeaderStyle settings, not stored on the entity. diff --git a/src/app/text_inline.rs b/src/app/text_inline.rs index c1f8b495e..a66754cf7 100644 --- a/src/app/text_inline.rs +++ b/src/app/text_inline.rs @@ -145,6 +145,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(); diff --git a/src/app/tolerance_dialog.rs b/src/app/tolerance_dialog.rs new file mode 100644 index 000000000..56e9b723c --- /dev/null +++ b/src/app/tolerance_dialog.rs @@ -0,0 +1,88 @@ +use acadrust::{EntityType, Handle}; + +impl super::OpenCADStudio { + pub(super) fn open_tolerance_dialog(&mut self, editing: Option) { + 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 mut command = + crate::modules::annotate::tolerance_cmd::ToleranceCommand::with_text(state.to_text()); + 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 + } +} diff --git a/src/app/update/command.rs b/src/app/update/command.rs index d6c48ab69..c097983b6 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1901,7 +1901,73 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } 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 +2618,27 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { 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::() { + 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), diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index a98bb6762..7266953bd 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -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(), @@ -3706,6 +3707,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; diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 0c52554a9..0fa5e27a7 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -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( diff --git a/src/entities/tolerance.rs b/src/entities/tolerance.rs index f293e59f5..6c64147b3 100644 --- a/src/entities/tolerance.rs +++ b/src/entities/tolerance.rs @@ -4,7 +4,7 @@ use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject}; -use crate::scene::model::object::{GripApply, GripDef, PropSection}; +use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; use crate::scene::text::lff; use crate::scene::view::transform; @@ -144,51 +144,6 @@ fn symbol_font_switch(inner: &str) -> Option { tail.chars().next() } -/// Per-entity overrides of individual dimension-style variables, carried as -/// extended data under the `DSTYLE` application. -/// -/// An entity may keep its style yet override single variables on itself, as -/// `(variable group code, value)` pairs. Reading the style but ignoring these -/// draws the frame at the style's size rather than its own — which is how this -/// one, overriding its text height to a fraction of the style's, came out far -/// too large. -/// -/// Only the character height is read — it is the frame's single geometric -/// input (see `tessellate_tolerance`); every other variable is left to the style. -fn dstyle_overrides(tol: &Tolerance) -> Option { - // The only variable the frame is built from. - const DIMTXT: i16 = 140; - - use acadrust::xdata::XDataValue as V; - - // The record belongs to the shared "ACAD" application and names itself in - // its FIRST STRING VALUE. There is no record called "DSTYLE" — asking for - // one finds nothing and leaves every override silently unread. - let rec = tol.common.extended_data.get_record("ACAD")?; - let mut vals = rec.values.iter(); - match vals.next() { - Some(V::String(s)) if s == "DSTYLE" => {} - _ => return None, - } - - let mut txt = None; - let mut pending: Option = None; - for v in vals { - match v { - V::Integer16(code) => pending = Some(*code), - V::Real(value) | V::Distance(value) => { - if pending.take() == Some(DIMTXT) { - txt = Some(*value); - } - } - // Braces and anything else just delimit; a non-numeric value also - // ends the pair we were waiting on. - _ => pending = None, - } - } - txt -} - /// The tolerance's dimension style — by handle first, then by name. /// /// The order matters and is not interchangeable: a DWG records the style's @@ -196,7 +151,7 @@ fn dstyle_overrides(tol: &Tolerance) -> Option { /// leaves the handle empty. Matching on the name alone — the shape used /// elsewhere for entities that only ever carry one — would silently resolve /// every DWG-read tolerance to "Standard" and pick the wrong metrics. -fn resolve_dim_style<'a>( +pub(crate) fn resolve_dim_style<'a>( tol: &Tolerance, doc: &'a acadrust::CadDocument, ) -> Option<&'a acadrust::tables::DimStyle> { @@ -222,8 +177,20 @@ fn resolve_dim_style<'a>( /// /// This is where the pen LANDS, so it carries the font's letter spacing past /// the final glyph — which is what puts the gap between one run and the next. -fn run_advance(text: &str, font: &str, height: f32) -> f32 { - crate::entities::text_support::text_local_bounds(font, text, height, 1.0, 0.0) +fn run_advance( + text: &str, + font: &str, + height: f32, + width_factor: f32, + oblique: f32, +) -> f32 { + crate::entities::text_support::text_local_bounds( + font, + text, + height, + width_factor, + oblique, + ) .map(|b| b.advance) .unwrap_or(0.0) } @@ -232,8 +199,11 @@ fn run_advance(text: &str, font: &str, height: f32) -> f32 { /// /// Glyph geometry is authored against a 9-unit cap height, so a font's spacing /// scales with the character height like everything else. -fn letter_spacing(font: &str, height: f32) -> f32 { - crate::scene::text::font_face::Face::resolve(font).letter_spacing() * height / 9.0 +fn letter_spacing(font: &str, height: f32, width_factor: f32) -> f32 { + crate::scene::text::font_face::Face::resolve(font).letter_spacing() + * height + / 9.0 + * width_factor } /// How wide a compartment's content actually draws. @@ -243,15 +213,35 @@ fn letter_spacing(font: &str, height: f32) -> f32 { /// over-measures by exactly one spacing — the gaps between runs are real, the /// one hanging off the end is not — and every compartment came out that much /// too wide. -fn content_width(cell: &Cell, height: f32) -> f32 { +fn content_width( + cell: &Cell, + height: f32, + text_style: &crate::entities::text_support::ResolvedTextStyle, +) -> f32 { let Some(last) = cell.last() else { return 0.0; }; let pen: f32 = cell .iter() - .map(|r| run_advance(&r.text, r.font, height)) + .map(|run| { + let (font, width, oblique) = if run.font == SYMBOL_FONT { + (SYMBOL_FONT, 1.0, 0.0) + } else { + ( + text_style.font_name.as_str(), + text_style.width_factor, + text_style.oblique_angle, + ) + }; + run_advance(&run.text, font, height, width, oblique) + }) .sum(); - (pen - letter_spacing(last.font, height)).max(0.0) + let (last_font, last_width) = if last.font == SYMBOL_FONT { + (SYMBOL_FONT, 1.0) + } else { + (text_style.font_name.as_str(), text_style.width_factor) + }; + (pen - letter_spacing(last_font, height, last_width)).max(0.0) } /// One text run of a feature-control frame, ready to become a `TextStroke` @@ -261,11 +251,13 @@ fn content_width(cell: &Cell, height: f32) -> f32 { /// origin (no origin translation — the wire-builder adds the origin). struct TolCell { text: String, - font: &'static str, + font: String, origin: [f32; 2], strokes: Vec>, height: f32, rotation: f32, + width_factor: f32, + oblique: f32, } /// Tessellate a Tolerance entity's feature-control frame. @@ -296,17 +288,29 @@ fn tessellate_tolerance( // and the frame draws about ten times too small. Falling back to them only // when no style resolves keeps DXF-read entities working. let style = resolve_dim_style(tol, doc); - let scale = style - .map(|s| if s.dimscale > 1e-6 { s.dimscale } else { 1.0 }) + let xd = &tol.common.extended_data; + let scale = crate::entities::dim_override::real(xd, crate::entities::dim_override::DIMSCALE) + .or_else(|| style.map(|s| s.dimscale)) + .map(|value| if value > 1e-6 { value } else { 1.0 }) .unwrap_or(1.0); // The entity's own overrides win over its style; the style wins over the // entity's constructed defaults. - let h = dstyle_overrides(tol) + let h = crate::entities::dim_override::real(xd, crate::entities::dim_override::DIMTXT) .or(style.map(|s| s.dimtxt)) .map(|v| v * scale) .unwrap_or(tol.text_height) as f32; let h = if h > 1e-6 { h } else { 2.5_f32 }; + let text_style_name = crate::entities::dim_override::handle( + xd, + crate::entities::dim_override::DIMTXSTY, + ) + .and_then(|handle| doc.text_styles.iter().find(|entry| entry.handle == handle)) + .map(|entry| entry.name.as_str()) + .or_else(|| style.map(|style| style.dimtxsty.as_str())) + .unwrap_or("Standard"); + let text_style = crate::entities::text_support::resolve_text_style(text_style_name, doc); + // A compartment is a PROPORTION of the character height: it spans -h..+h // about the row's centreline, so it is exactly 2h tall and the h/2 margin // falls out of that rather than being an input. @@ -329,7 +333,9 @@ fn tessellate_tolerance( // (`len()`) would make each one two or three cells wide. // A compartment's runs come from different fonts, so each is measured in its // own before they are summed. - let cell_width = |cell: &Cell| -> f32 { (content_width(cell, h) + 2.0 * pad).max(min_cell_w) }; + let cell_width = |cell: &Cell| -> f32 { + (content_width(cell, h, &text_style) + 2.0 * pad).max(min_cell_w) + }; let row_widths: Vec> = rows .iter() .map(|row| row.iter().map(|c| cell_width(c)).collect()) @@ -399,11 +405,28 @@ fn tessellate_tolerance( // Centre the compartment's whole content, then lay its runs out // left to right — each in its own font, each advancing the pen // by what that font actually measures. - let mut run_x = cell_x + (cw - content_width(cell, h)) * 0.5; + let mut run_x = cell_x + (cw - content_width(cell, h, &text_style)) * 0.5; for run in cell { - let (text, font) = (run.text.clone(), run.font); + let text = run.text.clone(); + let (font, width_factor, oblique) = if run.font == SYMBOL_FONT { + (SYMBOL_FONT.to_string(), 1.0, 0.0) + } else { + ( + text_style.font_name.clone(), + text_style.width_factor, + text_style.oblique_angle, + ) + }; let (local_strokes, _) = - lff::tessellate_text_ex([0.0, 0.0], h, 0.0, 1.0, 0.0, font, &text); + lff::tessellate_text_ex( + [0.0, 0.0], + h, + 0.0, + width_factor, + oblique, + &font, + &text, + ); // Glyph polylines rotated about the run's origin (no origin // translation — the wire-builder adds `origin`). let strokes: Vec> = local_strokes @@ -411,7 +434,7 @@ fn tessellate_tolerance( .map(|pl| pl.into_iter().map(|[px, py]| rot(px, py)).collect()) .filter(|pl: &Vec<[f32; 2]>| !pl.is_empty()) .collect(); - let advance = run_advance(&text, font, h); + let advance = run_advance(&text, &font, h, width_factor, oblique); cells.push(TolCell { text, font, @@ -419,6 +442,8 @@ fn tessellate_tolerance( strokes, height: h, rotation: angle, + width_factor, + oblique, }); run_x += advance; } @@ -447,6 +472,45 @@ impl RenderConvertible for Tolerance { // Build the feature-control frame in local space; origin stored as f64. let (box_strokes, cells) = tessellate_tolerance(self, document); let ins = [self.insertion_point.x, self.insertion_point.y]; + let style = resolve_dim_style(self, document); + let xd = &self.common.extended_data; + let explicit_color = |index: Option| { + index.filter(|value| !matches!(value, 0 | 256)).map(|value| { + let [red, green, blue, _] = crate::scene::convert::tess_util::aci_to_rgba( + &acadrust::types::Color::from_index(value), + ); + [red, green, blue] + }) + }; + let color_value = |color: &acadrust::types::Color| { + let [red, green, blue, _] = + crate::scene::convert::tess_util::aci_to_rgba(color); + [red, green, blue] + }; + let frame_color = if let Some(index) = crate::entities::dim_override::int( + xd, + crate::entities::dim_override::DIMCLRD, + ) { + explicit_color(Some(index)) + } else if let Some(color) = + style.and_then(|entry| entry.dimclrd_true_color.as_ref()) + { + Some(color_value(color)) + } else { + explicit_color(style.map(|entry| entry.dimclrd)) + }; + let text_color = if let Some(index) = crate::entities::dim_override::int( + xd, + crate::entities::dim_override::DIMCLRT, + ) { + explicit_color(Some(index)) + } else if let Some(color) = + style.and_then(|entry| entry.dimclrt_true_color.as_ref()) + { + Some(color_value(color)) + } else { + explicit_color(style.map(|entry| entry.dimclrt)) + }; // Frame geometry first (run-less → always strokes; also the anchor // group so its origin = the insertion point), then one run-group per @@ -457,7 +521,7 @@ impl RenderConvertible for Tolerance { groups.push(TextStroke { strokes: box_strokes, origin: ins, - color: None, + color: frame_color, fill_tris: vec![], run: None, }); @@ -469,15 +533,15 @@ impl RenderConvertible for Tolerance { ins[0] + cell.origin[0] as f64, ins[1] + cell.origin[1] as f64, ], - color: None, + color: text_color, fill_tris: vec![], run: Some(GlyphRun { text: cell.text, - font: cell.font.to_string(), + font: cell.font, height: cell.height, rotation: cell.rotation, - width_factor: 1.0, - oblique: 0.0, + width_factor: cell.width_factor, + oblique: cell.oblique, // `tracking` scales the font's own letter spacing, so 0 // collapses the gap between glyphs and the characters run // together. Every other text-bearing entity passes 1.0, and @@ -539,12 +603,19 @@ impl Grippable for Tolerance { // ── PropertyEditable ────────────────────────────────────────────────────────── impl PropertyEditable for Tolerance { - fn geometry_properties(&self, _text_style_names: &[String]) -> Vec { + fn geometry_properties(&self, text_style_names: &[String]) -> Vec { vec![ PropSection { title: t!("Text").into_owned(), props: vec![ - ro(t!("Text style").as_ref(), "tol_text_style", String::new()), + Property { + label: t!("Text style").into_owned(), + field: "tol_text_style", + value: PropValue::Choice { + selected: String::new(), + options: text_style_names.to_vec(), + }, + }, edit(t!("Text height").as_ref(), "tol_text_height", self.text_height), ], }, @@ -585,9 +656,29 @@ impl PropertyEditable for Tolerance { "tol_iy" => self.insertion_point.y = v, "tol_iz" => self.insertion_point.z = v, "tol_text_height" if v > 0.0 => self.text_height = v, - "tol_dir_x" => self.direction.x = v, - "tol_dir_y" => self.direction.y = v, - "tol_dir_z" => self.direction.z = v, + "tol_dir_x" | "tol_dir_y" | "tol_dir_z" => { + let mut candidate = self.direction; + match field { + "tol_dir_x" => candidate.x = v, + "tol_dir_y" => candidate.y = v, + _ => candidate.z = v, + } + let direction_len = + (candidate.x * candidate.x + candidate.y * candidate.y + candidate.z * candidate.z) + .sqrt(); + let normal_len = + (self.normal.x * self.normal.x + self.normal.y * self.normal.y + self.normal.z * self.normal.z) + .sqrt(); + let dot = candidate.x * self.normal.x + + candidate.y * self.normal.y + + candidate.z * self.normal.z; + if direction_len > 1.0e-9 + && (normal_len <= 1.0e-9 + || dot.abs() <= direction_len * normal_len * 1.0e-8) + { + self.direction = candidate; + } + } _ => {} } } @@ -923,9 +1014,26 @@ mod tests { fn compartments_are_measured_by_their_ink_not_the_trailing_pen_gap() { // Cap height, so glyph units and world units line up. let h = 9.0_f32; + let document = acadrust::CadDocument::new(); + let text_style = crate::entities::text_support::resolve_text_style("Standard", &document); for src in ["{\\Fgdt;r}", "{\\Fgdt;n}tol{\\Fgdt;m}", "1{\\Fgdt;m}", "A"] { let cell = parse_cell(src); - let pen: f32 = cell.iter().map(|r| run_advance(&r.text, r.font, h)).sum(); + let pen: f32 = cell + .iter() + .map(|run| { + if run.font == SYMBOL_FONT { + run_advance(&run.text, SYMBOL_FONT, h, 1.0, 0.0) + } else { + run_advance( + &run.text, + &text_style.font_name, + h, + text_style.width_factor, + text_style.oblique_angle, + ) + } + }) + .sum(); let ink: f32 = cell .iter() .map(|r| { @@ -934,11 +1042,17 @@ mod tests { .unwrap_or(0.0) }) .sum(); - let got = content_width(&cell, h); + let got = content_width(&cell, h, &text_style); // Exactly one trailing gap comes off — the gaps BETWEEN runs are // real spacing and must stay. - let spacing = letter_spacing(cell.last().unwrap().font, h); + let last = cell.last().unwrap(); + let (font, width) = if last.font == SYMBOL_FONT { + (SYMBOL_FONT, 1.0) + } else { + (text_style.font_name.as_str(), text_style.width_factor) + }; + let spacing = letter_spacing(font, h, width); assert!( (got - (pen - spacing)).abs() < 1e-3, "{src:?}: content {got} should be pen {pen} less one {spacing} gap" diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index d27927eee..911d9dab5 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -1,8 +1,6 @@ // TOLERANCE command — place a GD&T (geometric dimensioning & tolerancing) frame. // -// Workflow: -// 1. Text: Enter tolerance string (e.g. "%%v0.05|A" or plain text) -// 2. Point: Click insertion point +// The structured editor prepares the frame; this command places it. use acadrust::entities::Tolerance; use acadrust::types::Vector3; @@ -25,20 +23,15 @@ pub fn tool() -> ToolDef { } } -enum Step { - Text, - Insertion { text: String }, -} - pub struct ToleranceCommand { - step: Step, + text: String, plane: WorkingPlane, } impl ToleranceCommand { - pub fn new() -> Self { + pub fn with_text(text: String) -> Self { Self { - step: Step::Text, + text, plane: WorkingPlane::default(), } } @@ -54,36 +47,18 @@ impl CadCommand for ToleranceCommand { } fn prompt(&self) -> String { - match &self.step { - Step::Text => t!("TOLERANCE Enter tolerance text:").into_owned(), - Step::Insertion { text } => { - t!("TOLERANCE Specify insertion point [%{text}]:", text = text).into_owned() - } - } + t!("TOLERANCE Specify insertion point:").into_owned() } fn wants_text_input(&self) -> bool { - matches!(self.step, Step::Text) - } - - fn on_text_input(&mut self, text: &str) -> Option { - let t = text.trim().to_string(); - if t.is_empty() { - return Some(CmdResult::Cancel); - } - self.step = Step::Insertion { text: t }; - Some(CmdResult::NeedPoint) + false } fn on_point(&mut self, pt: DVec3) -> CmdResult { - if let Step::Insertion { text } = &self.step { - let point = self.plane.to_local(pt); - let ins = Vector3::new(point.x, point.y, point.z); - let tol = Tolerance::with_text(ins, text.clone()); - CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Tolerance(tol))) - } else { - CmdResult::NeedPoint - } + let point = self.plane.to_local(pt); + let ins = Vector3::new(point.x, point.y, point.z); + let tol = Tolerance::with_text(ins, self.text.clone()); + CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Tolerance(tol))) } fn on_enter(&mut self) -> CmdResult { @@ -91,17 +66,49 @@ impl CadCommand for ToleranceCommand { } fn on_mouse_move(&mut self, pt: DVec3) -> Option { - if !matches!(self.step, Step::Insertion { .. }) { - return None; + let normalized = self + .text + .replace("^J", "\n") + .replace("\\P", "\n") + .replace("\r\n", "\n") + .replace('\r', "\n"); + let rows: Vec> = normalized + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| { + let mut cells: Vec<&str> = line.split("%%v").collect(); + while cells.last().is_some_and(|cell| cell.trim().is_empty()) { + cells.pop(); + } + cells + }) + .filter(|cells| !cells.is_empty()) + .collect(); + let row_height = 0.35; + let cell_width = 0.5; + let mut points = Vec::new(); + let mut segment = |a: DVec3, b: DVec3| { + if !points.is_empty() { + points.push(DVec3::splat(f64::NAN)); + } + points.push(a); + points.push(b); + }; + for (row_index, cells) in rows.iter().enumerate() { + let count = cells.len().max(1); + let y0 = -row_height * (row_index as f64 + 0.5); + let y1 = y0 + row_height; + let x1 = cell_width * count as f64; + let p = |x: f64, y: f64| pt + self.plane.x * x + self.plane.y * y; + segment(p(0.0, y0), p(x1, y0)); + segment(p(x1, y0), p(x1, y1)); + segment(p(x1, y1), p(0.0, y1)); + segment(p(0.0, y1), p(0.0, y0)); + for index in 1..count { + let x = cell_width * index as f64; + segment(p(x, y0), p(x, y1)); + } } - let d = 0.15; - let points = [ - pt - self.plane.x * d, - pt + self.plane.x * d, - DVec3::splat(f64::NAN), - pt - self.plane.y * d, - pt + self.plane.y * d, - ]; Some(WireModel { point_marker: None, taper_widths: Vec::new(), @@ -119,10 +126,7 @@ impl CadCommand for ToleranceCommand { dash_align_end: None, text_verts: Vec::new(), name: "tolerance_preview".into(), - points: points - .iter() - .map(|point| point.as_vec3().to_array()) - .collect(), + points: points.iter().map(|point| point.as_vec3().to_array()).collect(), points_low: Vec::new(), color: WireModel::CYAN, selected: false, diff --git a/src/scene/cache/properties.rs b/src/scene/cache/properties.rs index fe199e881..2dbe971e1 100644 --- a/src/scene/cache/properties.rs +++ b/src/scene/cache/properties.rs @@ -94,7 +94,9 @@ pub fn general_section(entity: &EntityType) -> PropSection { ], }; - if matches!(entity, EntityType::LwPolyline(polyline) if crate::entities::lwpolyline::is_rectangle(polyline)) { + if matches!(entity, EntityType::LwPolyline(polyline) if crate::entities::lwpolyline::is_rectangle(polyline)) + || matches!(entity, EntityType::Tolerance(_)) + { section.props.retain(|prop| prop.field != "handle"); } diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index d249f80b1..5ce5e56db 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -416,6 +416,7 @@ fn tessellate_entity_inner( | EntityType::MText(_) | EntityType::Dimension(_) | EntityType::MultiLeader(_) + | EntityType::Tolerance(_) ) { crate::scene::annotative::effective_annotation_scale_for( document, diff --git a/src/ui/window/geometric_tolerance.rs b/src/ui/window/geometric_tolerance.rs new file mode 100644 index 000000000..214f4abda --- /dev/null +++ b/src/ui/window/geometric_tolerance.rs @@ -0,0 +1,479 @@ +//! Structured feature-control-frame editor. +//! +//! The entity stores a compact escape string, but users work with named +//! symbols, tolerance compartments and datum references. This dialog keeps +//! that storage detail behind typed controls and can also reopen existing +//! frames without losing their compartment layout. + +use std::fmt; + +use acadrust::Handle; +use iced::widget::{button, checkbox, column, container, row, text, text_input, Space}; +use iced::{Border, Element, Fill, Length, Theme}; + +use crate::app::Message; +use crate::t; + +const EMPTY: &str = ""; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ToleranceEntry { + pub diameter: bool, + pub value: String, + pub material: String, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct DatumEntry { + pub value: String, + pub material: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct State { + pub editing: Option, + pub symbol: String, + pub tolerances: [ToleranceEntry; 2], + pub datums: [DatumEntry; 3], + pub projected_height: String, + pub projected_zone: bool, + pub datum_identifier: String, +} + +impl Default for State { + fn default() -> Self { + Self { + editing: None, + symbol: String::new(), + tolerances: std::array::from_fn(|_| ToleranceEntry::default()), + datums: std::array::from_fn(|_| DatumEntry::default()), + projected_height: String::new(), + projected_zone: false, + datum_identifier: String::new(), + } + } +} + +#[derive(Clone, Debug)] +pub enum Field { + Symbol(String), + ToleranceValue(usize, String), + ToleranceMaterial(usize, String), + DatumValue(usize, String), + DatumMaterial(usize, String), + ProjectedHeight(String), + DatumIdentifier(String), +} + +#[derive(Clone, Debug)] +pub enum Toggle { + Diameter(usize, bool), + ProjectedZone(bool), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Choice { + code: &'static str, + label: &'static str, +} + +impl fmt::Display for Choice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(crate::i18n::translate(self.label).as_ref()) + } +} + +const SYMBOLS: [(&str, &str); 15] = [ + ("", "None"), + ("u", "Straightness"), + ("c", "Flatness"), + ("e", "Circularity"), + ("g", "Cylindricity"), + ("d", "Profile of a surface"), + ("k", "Profile of a line"), + ("j", "Position"), + ("r", "Concentricity"), + ("i", "Symmetry"), + ("f", "Parallelism"), + ("b", "Perpendicularity"), + ("a", "Angularity"), + ("h", "Circular runout"), + ("t", "Total runout"), +]; + +const MATERIALS: [(&str, &str); 4] = [ + ("", "None"), + ("m", "Maximum material condition"), + ("l", "Least material condition"), + ("s", "Regardless of feature size"), +]; + +fn choices(values: &'static [(&'static str, &'static str)]) -> Vec { + values + .iter() + .map(|(code, label)| Choice { code, label }) + .collect() +} + +fn selected(values: &'static [(&'static str, &'static str)], code: &str) -> Option { + values + .iter() + .find(|(value, _)| value.eq_ignore_ascii_case(code)) + .map(|(code, label)| Choice { code, label }) +} + +fn escape(code: &str) -> String { + if code.is_empty() { + String::new() + } else { + format!("{{\\Fgdt;{code}}}") + } +} + +fn strip_escape(input: &str, code: &str) -> Option { + let marker = escape(code); + input + .strip_suffix(&marker) + .map(std::string::ToString::to_string) +} + +fn parse_material(input: &str) -> (String, String) { + for code in ["m", "l", "s"] { + if let Some(value) = strip_escape(input, code) { + return (value, code.to_string()); + } + } + (input.to_string(), String::new()) +} + +impl State { + pub fn from_text(editing: Option, raw: &str) -> Self { + let mut state = Self { + editing, + ..Self::default() + }; + let normalized = raw + .replace("^J", "\n") + .replace("\\P", "\n") + .replace("\r\n", "\n") + .replace('\r', "\n"); + let mut lines = normalized.lines(); + + if let Some(frame) = lines.next() { + let cells: Vec<&str> = frame.split("%%v").collect(); + if let Some(cell) = cells.first() { + state.symbol = SYMBOLS + .iter() + .find_map(|(code, _)| { + (!code.is_empty() && cell.contains(&escape(code))).then(|| code.to_string()) + }) + .unwrap_or_default(); + } + for index in 0..2 { + if let Some(cell) = cells.get(index + 1) { + let (cell, diameter) = if let Some(rest) = cell.strip_prefix(&escape("n")) { + (rest, true) + } else { + (*cell, false) + }; + let (value, material) = parse_material(cell); + state.tolerances[index] = ToleranceEntry { + diameter, + value, + material, + }; + } + } + for index in 0..3 { + if let Some(cell) = cells.get(index + 3) { + let (value, material) = parse_material(cell); + state.datums[index] = DatumEntry { value, material }; + } + } + if cells.len() == 1 + && state.symbol.is_empty() + && !frame.trim().is_empty() + { + state.tolerances[0].value = frame.trim().to_string(); + } + } + + if let Some(projected) = lines.next() { + if let Some(value) = strip_escape(projected, "p") { + state.projected_height = value; + state.projected_zone = true; + } else { + state.projected_height = projected.to_string(); + } + } + if let Some(identifier) = lines.next() { + state.datum_identifier = identifier.to_string(); + } + state + } + + pub fn apply_field(&mut self, field: Field) { + match field { + Field::Symbol(value) => self.symbol = value, + Field::ToleranceValue(index, value) if index < 2 => { + self.tolerances[index].value = value + } + Field::ToleranceMaterial(index, value) if index < 2 => { + self.tolerances[index].material = value + } + Field::DatumValue(index, value) if index < 3 => self.datums[index].value = value, + Field::DatumMaterial(index, value) if index < 3 => { + self.datums[index].material = value + } + Field::ProjectedHeight(value) => self.projected_height = value, + Field::DatumIdentifier(value) => self.datum_identifier = value, + _ => {} + } + } + + pub fn apply_toggle(&mut self, toggle: Toggle) { + match toggle { + Toggle::Diameter(index, value) if index < 2 => { + self.tolerances[index].diameter = value + } + Toggle::ProjectedZone(value) => self.projected_zone = value, + _ => {} + } + } + + pub fn is_valid(&self) -> bool { + !self.symbol.is_empty() + || self.tolerances.iter().any(|entry| !entry.value.trim().is_empty()) + || self.datums.iter().any(|entry| !entry.value.trim().is_empty()) + || !self.projected_height.trim().is_empty() + || !self.datum_identifier.trim().is_empty() + } + + pub fn to_text(&self) -> String { + let mut cells = Vec::with_capacity(6); + cells.push(escape(&self.symbol)); + for entry in &self.tolerances { + let mut value = String::new(); + if entry.diameter && !entry.value.trim().is_empty() { + value.push_str(&escape("n")); + } + value.push_str(entry.value.trim()); + if !entry.value.trim().is_empty() { + value.push_str(&escape(&entry.material)); + } + cells.push(value); + } + for entry in &self.datums { + let mut value = entry.value.trim().to_string(); + if !value.is_empty() { + value.push_str(&escape(&entry.material)); + } + cells.push(value); + } + while cells.last().is_some_and(String::is_empty) { + cells.pop(); + } + let mut rows = vec![cells.join("%%v")]; + if !self.projected_height.trim().is_empty() || self.projected_zone { + let mut projected = self.projected_height.trim().to_string(); + if self.projected_zone { + projected.push_str(&escape("p")); + } + rows.push(projected); + } + if !self.datum_identifier.trim().is_empty() { + if rows.len() == 1 { + rows.push(String::new()); + } + rows.push(self.datum_identifier.trim().to_string()); + } + rows.join("\n") + } +} + +fn muted(theme: &Theme) -> iced::widget::text::Style { + iced::widget::text::Style { + color: Some(theme.palette().background.base.text.scale_alpha(0.65)), + } +} + +fn panel<'a>(title: String, body: Element<'a, Message>) -> Element<'a, Message> { + container(column![text(title).size(11).style(muted), body].spacing(6)) + .padding(8) + .width(Fill) + .style(|theme: &Theme| container::Style { + border: Border { + width: 1.0, + radius: 4.0.into(), + color: theme.palette().background.strong.color, + }, + ..Default::default() + }) + .into() +} + +fn material_picker<'a>(index: usize, datum: bool, code: &str) -> Element<'a, Message> { + let options = choices(&MATERIALS); + let selected = selected(&MATERIALS, code); + iced::widget::pick_list(selected, options, |choice| choice.to_string()) + .on_select(move |choice| { + if datum { + Message::ToleranceDialogField(Field::DatumMaterial( + index, + choice.code.to_string(), + )) + } else { + Message::ToleranceDialogField(Field::ToleranceMaterial( + index, + choice.code.to_string(), + )) + } + }) + .text_size(11) + .padding([3, 6]) + .width(Length::Fixed(190.0)) + .into() +} + +pub fn view_window<'a>( + state: &State, + sizing: crate::ui::modal::ModalSizing, +) -> Element<'a, Message> { + let symbol_options = choices(&SYMBOLS); + let symbol_selected = selected(&SYMBOLS, &state.symbol); + let symbol = panel( + t!("Geometric characteristic").into_owned(), + iced::widget::pick_list(symbol_selected, symbol_options, |choice| choice.to_string()) + .on_select(|choice| { + Message::ToleranceDialogField(Field::Symbol(choice.code.to_string())) + }) + .text_size(12) + .padding([4, 6]) + .width(Fill) + .into(), + ); + + let tolerance_rows = state.tolerances.iter().enumerate().fold( + column![].spacing(6), + |column, (index, entry)| { + column.push( + row![ + text(format!("{} {}", t!("Tolerance"), index + 1)) + .size(11) + .style(muted) + .width(Length::Fixed(82.0)), + checkbox(entry.diameter) + .on_toggle(move |value| Message::ToleranceDialogToggle( + Toggle::Diameter(index, value) + )) + .size(14), + text(t!("Diameter")).size(11).width(Length::Fixed(62.0)), + text_input(EMPTY, &entry.value) + .on_input(move |value| Message::ToleranceDialogField( + Field::ToleranceValue(index, value) + )) + .size(12) + .padding([3, 6]) + .width(Length::Fixed(125.0)), + material_picker(index, false, &entry.material), + ] + .spacing(6) + .align_y(iced::Center), + ) + }, + ); + let tolerances = panel(t!("Tolerance values").into_owned(), tolerance_rows.into()); + + let datum_rows = state.datums.iter().enumerate().fold( + column![].spacing(6), + |column, (index, entry)| { + column.push( + row![ + text(format!("{} {}", t!("Datum"), index + 1)) + .size(11) + .style(muted) + .width(Length::Fixed(82.0)), + text_input(EMPTY, &entry.value) + .on_input(move |value| Message::ToleranceDialogField( + Field::DatumValue(index, value) + )) + .size(12) + .padding([3, 6]) + .width(Length::Fixed(212.0)), + material_picker(index, true, &entry.material), + ] + .spacing(6) + .align_y(iced::Center), + ) + }, + ); + let datums = panel(t!("Datum references").into_owned(), datum_rows.into()); + + let additions = panel( + t!("Additional information").into_owned(), + column![ + row![ + text(t!("Projected height")) + .size(11) + .style(muted) + .width(Length::Fixed(112.0)), + text_input(EMPTY, &state.projected_height) + .on_input(|value| Message::ToleranceDialogField(Field::ProjectedHeight(value))) + .size(12) + .padding([3, 6]) + .width(Length::Fixed(125.0)), + checkbox(state.projected_zone) + .on_toggle(|value| Message::ToleranceDialogToggle( + Toggle::ProjectedZone(value) + )) + .size(14), + text(t!("Projected tolerance zone")).size(11), + ] + .spacing(6) + .align_y(iced::Center), + row![ + text(t!("Datum identifier")) + .size(11) + .style(muted) + .width(Length::Fixed(112.0)), + text_input(EMPTY, &state.datum_identifier) + .on_input(|value| Message::ToleranceDialogField(Field::DatumIdentifier(value))) + .size(12) + .padding([3, 6]) + .width(Length::Fixed(212.0)), + ] + .spacing(6) + .align_y(iced::Center), + ] + .spacing(6) + .into(), + ); + + let mut actions = row![ + Space::new().width(Fill), + button(text(t!("Cancel")).size(12)) + .on_press(Message::CloseModal) + .padding([4, 12]), + ] + .spacing(6) + .align_y(iced::Center); + if state.editing.is_some() { + actions = actions.push( + button(text(t!("Apply")).size(12)) + .on_press_maybe(state.is_valid().then_some(Message::ToleranceDialogApply)) + .padding([4, 12]), + ); + } + actions = actions.push( + button(text(t!("OK")).size(12)) + .on_press_maybe(state.is_valid().then_some(Message::ToleranceDialogOk)) + .style(button::primary) + .padding([4, 12]), + ); + + column![symbol, tolerances, datums, additions, actions] + .spacing(8) + .padding(10) + .width(sizing.width) + .into() +} diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index e1ae0b229..c4e345c8d 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -3,6 +3,7 @@ pub mod block_palette; pub mod layout_manager; pub mod layer_state_manager; pub mod drawing_units; +pub mod geometric_tolerance; pub mod drafting_settings; pub mod layer_translator; pub mod plot;