Complete geometric tolerance workflow and properties
This commit is contained in:
parent
8c61d89b1e
commit
de294447a9
14 changed files with 1037 additions and 135 deletions
|
|
@ -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" => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<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 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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
88
src/app/tolerance_dialog.rs
Normal file
88
src/app/tolerance_dialog.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1901,7 +1901,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 +2618,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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<char> {
|
|||
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<f64> {
|
||||
// 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<i16> = 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<f64> {
|
|||
/// 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<Vec<[f32; 2]>>,
|
||||
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<Vec<f32>> = 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<Vec<[f32; 2]>> = 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<i16>| {
|
||||
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<PropSection> {
|
||||
fn geometry_properties(&self, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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<CmdResult> {
|
||||
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<WireModel> {
|
||||
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<Vec<&str>> = 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,
|
||||
|
|
|
|||
4
src/scene/cache/properties.rs
vendored
4
src/scene/cache/properties.rs
vendored
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -416,6 +416,7 @@ fn tessellate_entity_inner(
|
|||
| EntityType::MText(_)
|
||||
| EntityType::Dimension(_)
|
||||
| EntityType::MultiLeader(_)
|
||||
| EntityType::Tolerance(_)
|
||||
) {
|
||||
crate::scene::annotative::effective_annotation_scale_for(
|
||||
document,
|
||||
|
|
|
|||
479
src/ui/window/geometric_tolerance.rs
Normal file
479
src/ui/window/geometric_tolerance.rs
Normal file
|
|
@ -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<Handle>,
|
||||
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<Choice> {
|
||||
values
|
||||
.iter()
|
||||
.map(|(code, label)| Choice { code, label })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn selected(values: &'static [(&'static str, &'static str)], code: &str) -> Option<Choice> {
|
||||
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<String> {
|
||||
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<Handle>, 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()
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue