diff --git a/src/entities/dim_override.rs b/src/entities/dim_override.rs index f7bca09ad..13ce51b8a 100644 --- a/src/entities/dim_override.rs +++ b/src/entities/dim_override.rs @@ -77,7 +77,12 @@ pub const DIMATFIT: i16 = 289; pub const DIMFXLON: i16 = 290; pub const DIMTFILL: i16 = 69; pub const DIMTFILLCLR: i16 = 70; -pub const DIMTXTDIRECTION: i16 = 295; +pub const DIMTXTDIRECTION: i16 = 294; +pub const DIMALTMZF: i16 = 295; +pub const DIMALTMZS: i16 = 296; +pub const DIMMZF: i16 = 297; +pub const DIMMZS: i16 = 298; +pub const DIMTALN: i16 = 392; pub const DIMTXSTY: i16 = 340; pub const DIMBLK: i16 = 342; pub const DIMBLK1: i16 = 343; @@ -243,7 +248,9 @@ pub fn property_real_code(field: &str) -> Option { "dim_roundoff" => DIMRND, "dim_scale_linear" => DIMLFAC, "dim_alt_scale_factor" => DIMALTF, + "dim_alt_sub_units_scale" => DIMALTMZF, "dim_alt_roundoff" => DIMALTRND, + "dim_sub_units_scale" => DIMMZF, "dim_tolerance_limit_lower" => DIMTM, "dim_tolerance_limit_upper" => DIMTP, "dim_tolerance_text_height" => DIMTFAC, @@ -268,21 +275,23 @@ pub fn property_int_code(field: &str) -> Option { "dim_units" => DIMLUNIT, "dim_precision" => DIMDEC, "dim_decimal_separator" => DIMDSEP, - "dim_angle_format" => DIMAUNIT, - "dim_angle_precision" => DIMADEC, + "dim_fractional_type" => DIMFRAC, + "dim_text_view_direction" => DIMTXTDIRECTION, "dim_alt_enabled" => DIMALT, "dim_alt_format" => DIMALTU, "dim_alt_precision" => DIMALTD, + "dim_alt_tolerance_precision" => DIMALTTD, "dim_tolerance_precision" => DIMTDEC, "dim_tolerance_pos_vert" => DIMTOLJ, + "dim_tolerance_alignment" => DIMTALN, _ => return None, }) } pub fn property_string_code(field: &str) -> Option { Some(match field { - "dim_prefix_suffix" => DIMPOST, - "dim_alt_prefix_suffix" => DIMAPOST, + "dim_sub_units_suffix" => DIMMZS, + "dim_alt_sub_units_suffix" => DIMALTMZS, _ => return None, }) } @@ -317,12 +326,63 @@ fn inherited_int(doc: &CadDocument, handle: Handle, code: i16) -> i16 { DIMZIN => style.dimzin, DIMALTZ => style.dimaltz, DIMTZIN => style.dimtzin, + DIMALTTZ => style.dimalttz, DIMTOL => style.dimtol as i16, DIMLIM => style.dimlim as i16, + DIMALT => style.dimalt as i16, + DIMTFILL => style.dimtfill, + DIMTALN => 0, _ => 0, } } +fn inherited_real(doc: &CadDocument, handle: Handle, code: i16) -> f64 { + let Some(EntityType::Dimension(dimension)) = doc.get_entity(handle) else { + return 0.0; + }; + if let Some(value) = real(&dimension.base().common.extended_data, code) { + return value; + } + let Some(style) = doc.dim_styles.iter().find(|style| { + style.name.eq_ignore_ascii_case(&dimension.base().style_name) + || (dimension.base().style_name.trim().is_empty() + && style.name.eq_ignore_ascii_case("Standard")) + }) else { + return 0.0; + }; + match code { + DIMGAP => style.dimgap, + DIMTP => style.dimtp, + DIMTM => style.dimtm, + _ => 0.0, + } +} + +fn inherited_string(doc: &CadDocument, handle: Handle, code: i16) -> String { + let Some(EntityType::Dimension(dimension)) = doc.get_entity(handle) else { + return String::new(); + }; + if let Some(value) = string(&dimension.base().common.extended_data, code) { + return value; + } + let Some(style) = doc.dim_styles.iter().find(|style| { + style.name.eq_ignore_ascii_case(&dimension.base().style_name) + || (dimension.base().style_name.trim().is_empty() + && style.name.eq_ignore_ascii_case("Standard")) + }) else { + return String::new(); + }; + match code { + DIMPOST => style.dimpost.clone(), + DIMAPOST => style.dimapost.clone(), + _ => String::new(), + } +} + +fn split_template(value: &str) -> (&str, &str) { + value.split_once("<>").unwrap_or(("", value)) +} + pub fn set_property( doc: &mut CadDocument, handle: Handle, @@ -341,14 +401,16 @@ pub fn set_property( }; if let Some(code) = handle_field { let resolved = match field { - "dim_arrowhead_1" | "dim_arrowhead_2" => (trimmed != "Closed filled") - .then(|| { + "dim_arrowhead_1" | "dim_arrowhead_2" => { + if trimmed == "Closed filled" { + Some(Handle::NULL) + } else { doc.block_records .iter() .find(|record| record.name == trimmed) .map(|record| record.handle) - }) - .flatten(), + } + } "dim_text_style" => doc .text_styles .iter() @@ -360,7 +422,10 @@ pub fn set_property( .find(|line_type| line_type.name == trimmed) .map(|line_type| line_type.handle), }; - set(doc, handle, code, resolved.map(XDataValue::Handle)); + let Some(resolved) = resolved else { + return false; + }; + set(doc, handle, code, Some(XDataValue::Handle(resolved))); return true; } let inverse_boolean_code = match field { @@ -384,6 +449,8 @@ pub fn set_property( "dim_alt_suppress_trailing_zeros" => Some((DIMALTZ, 8)), "dim_tolerance_suppress_leading_zeros" => Some((DIMTZIN, 4)), "dim_tolerance_suppress_trailing_zeros" => Some((DIMTZIN, 8)), + "dim_alt_tolerance_suppress_leading_zeros" => Some((DIMALTTZ, 4)), + "dim_alt_tolerance_suppress_trailing_zeros" => Some((DIMALTTZ, 8)), _ => None, }; if let Some((code, bit)) = bit_field { @@ -399,21 +466,124 @@ pub fn set_property( set(doc, handle, code, Some(XDataValue::Integer16(value))); return true; } + let zero_base = match field { + "dim_suppress_zero_feet" => Some((DIMZIN, true)), + "dim_suppress_zero_inches" => Some((DIMZIN, false)), + "dim_alt_suppress_zero_feet" => Some((DIMALTZ, true)), + "dim_alt_suppress_zero_inches" => Some((DIMALTZ, false)), + "dim_tolerance_suppress_zero_feet" => Some((DIMTZIN, true)), + "dim_tolerance_suppress_zero_inches" => Some((DIMTZIN, false)), + "dim_alt_tolerance_suppress_zero_feet" => Some((DIMALTTZ, true)), + "dim_alt_tolerance_suppress_zero_inches" => Some((DIMALTTZ, false)), + _ => None, + }; + if let Some((code, feet_field)) = zero_base { + let Some(enabled) = yes(trimmed).map(|value| value != 0) else { + return false; + }; + let current = inherited_int(doc, handle, code); + let mut suppress_feet = matches!(current & 3, 0 | 3); + let mut suppress_inches = matches!(current & 3, 0 | 2); + if feet_field { + suppress_feet = enabled; + } else { + suppress_inches = enabled; + } + let base = match (suppress_feet, suppress_inches) { + (true, true) => 0, + (false, false) => 1, + (false, true) => 2, + (true, false) => 3, + }; + set( + doc, + handle, + code, + Some(XDataValue::Integer16((current & !3) | base)), + ); + return true; + } + if field == "dim_text_fill_color" { + let mode = match trimmed.to_ascii_lowercase().as_str() { + "none" => 0, + "background" => 1, + "color" => 2, + _ => return false, + }; + set(doc, handle, DIMTFILL, Some(XDataValue::Integer16(mode))); + return true; + } + if matches!( + field, + "dim_prefix" | "dim_suffix" | "dim_alt_prefix" | "dim_alt_suffix" + ) { + let code = if field.starts_with("dim_alt_") { + DIMAPOST + } else { + DIMPOST + }; + let current = inherited_string(doc, handle, code); + let (old_prefix, old_suffix) = split_template(¤t); + let (prefix, suffix) = if field.ends_with("prefix") { + (value, old_suffix) + } else { + (old_prefix, value) + }; + set( + doc, + handle, + code, + Some(XDataValue::String(format!("{prefix}<>{suffix}"))), + ); + return true; + } if field == "dim_tolerance_display" { - let (tolerance, limits) = match trimmed.to_ascii_lowercase().as_str() { - "deviation" => (1, 0), - "limits" => (0, 1), - "none" => (0, 0), + let current_gap = inherited_real(doc, handle, DIMGAP).abs(); + let upper = inherited_real(doc, handle, DIMTP); + let lower = inherited_real(doc, handle, DIMTM); + let (tolerance, limits, basic) = match trimmed.to_ascii_lowercase().as_str() { + "symmetrical" => { + set(doc, handle, DIMTM, Some(XDataValue::Real(upper))); + (1, 0, false) + } + "deviation" => { + if (upper - lower).abs() <= 1e-12 { + let distinct_lower = if upper.abs() > 1e-12 { 0.0 } else { 0.0001 }; + set( + doc, + handle, + DIMTM, + Some(XDataValue::Real(distinct_lower)), + ); + } + (1, 0, false) + } + "limits" => (0, 1, false), + "basic" => (0, 0, true), + "none" => (0, 0, false), _ => return false, }; set(doc, handle, DIMTOL, Some(XDataValue::Integer16(tolerance))); set(doc, handle, DIMLIM, Some(XDataValue::Integer16(limits))); + let gap = if basic { + -current_gap.max(0.0001) + } else { + current_gap + }; + set(doc, handle, DIMGAP, Some(XDataValue::Real(gap))); return true; } if let Some(code) = property_real_code(field) { if trimmed.is_empty() { set(doc, handle, code, None); } else if let Ok(number) = trimmed.parse::() { + let number = if field == "dim_text_offset" + && inherited_real(doc, handle, DIMGAP) < 0.0 + { + -number.abs() + } else { + number + }; set(doc, handle, code, Some(XDataValue::Real(number))); } else { return false; @@ -431,6 +601,9 @@ pub fn set_property( } if let Some(code) = property_int_code(field) { let parsed = match field { + "dim_line_lineweight" | "dim_ext_line_lineweight" => { + parse_lineweight_label(trimmed) + } "dim_ext_line_fixed" | "dim_text_outside_align" | "dim_text_inside_align" @@ -451,12 +624,15 @@ pub fn set_property( "desktop" => Some(6), _ => trimmed.parse().ok(), }, - "dim_angle_format" => match trimmed.to_ascii_lowercase().as_str() { - "decimal degrees" => Some(0), - "degrees/minutes/seconds" => Some(1), - "gradians" => Some(2), - "radians" => Some(3), - "surveyors units" => Some(4), + "dim_fractional_type" => match trimmed.to_ascii_lowercase().as_str() { + "horizontal" => Some(0), + "diagonal" => Some(1), + "not stacked" => Some(2), + _ => trimmed.parse().ok(), + }, + "dim_text_view_direction" => match trimmed.to_ascii_lowercase().as_str() { + "left-to-right" => Some(0), + "right-to-left" => Some(1), _ => trimmed.parse().ok(), }, "dim_text_pos_vert" => match trimmed.to_ascii_lowercase().as_str() { @@ -476,17 +652,27 @@ pub fn set_property( _ => trimmed.parse().ok(), }, "dim_fit" => match trimmed.to_ascii_lowercase().as_str() { - "either text or arrows" => Some(0), + "both text and arrows" => Some(0), "arrows" => Some(1), "text" => Some(2), - "both text and arrows" => Some(3), - "always keep text" => Some(4), + "best fit" => Some(3), _ => trimmed.parse().ok(), }, "dim_text_movement" => match trimmed.to_ascii_lowercase().as_str() { - "move dimension line" => Some(0), - "add leader" => Some(1), - "move text freely" => Some(2), + "keep dim line with text" => Some(0), + "move text, add leader" => Some(1), + "move text, no leader" => Some(2), + _ => trimmed.parse().ok(), + }, + "dim_alt_format" => match trimmed.to_ascii_lowercase().as_str() { + "scientific" => Some(1), + "decimal" => Some(2), + "engineering" => Some(3), + "architectural stacked" => Some(4), + "fractional stacked" => Some(5), + "architectural" => Some(6), + "fractional" => Some(7), + "desktop" => Some(8), _ => trimmed.parse().ok(), }, "dim_tolerance_pos_vert" => match trimmed.to_ascii_lowercase().as_str() { @@ -495,6 +681,11 @@ pub fn set_property( "top" => Some(2), _ => trimmed.parse().ok(), }, + "dim_tolerance_alignment" => match trimmed.to_ascii_lowercase().as_str() { + "align decimal separators" => Some(0), + "align operational symbols" => Some(1), + _ => trimmed.parse().ok(), + }, _ => trimmed.parse().ok(), }; let Some(number) = parsed else { @@ -505,3 +696,17 @@ pub fn set_property( } false } + +fn parse_lineweight_label(value: &str) -> Option { + match value.trim() { + "ByLayer" => Some(-1), + "ByBlock" => Some(-2), + "Default" => Some(-3), + value => value + .trim_end_matches("mm") + .trim() + .parse::() + .ok() + .map(|millimetres| (millimetres * 100.0).round() as i16), + } +} diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index d5e9c4d1b..d95fa311e 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -12,7 +12,7 @@ use crate::entities::common::{ lineweight_options, parse_f64, ro_prop as ro, square_grip, }; use crate::entities::traits::{Grippable, PropertyEditable, Transformable}; -use crate::scene::model::object::{GripApply, GripDef, PropSection}; +use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::t; fn base_props(base: &DimensionBase) -> Vec { @@ -73,23 +73,6 @@ fn properties(dim: &Dimension) -> Vec { base.style_name.clone(), ), }, - ro( - t!("Measurement").as_ref(), - "measurement", - format!("{:.4}", d.measurement()), - ), - crate::scene::model::object::Property { - label: t!("Text override").into_owned(), - field: "text_override", - value: crate::scene::model::object::PropValue::PlainText( - base.text_override().unwrap_or("").to_string(), - ), - }, - edit_angle( - t!("Text rotation").as_ref(), - "text_rotation", - base.text_rotation.to_degrees(), - ), edit_angle(t!("Dim line angle").as_ref(), "rotation", d.rotation.to_degrees()), edit_angle( t!("Extension line angle").as_ref(), @@ -1429,360 +1412,135 @@ use acadrust::entities::{MText, Text}; use acadrust::tables::DimStyle; use acadrust::types::{Color as AcadColor, Vector3}; -/// Dimension-style-derived property groups (Lines & Arrows, Text, Fit, Primary -/// Units, Alternate Units, Tolerances) built from the resolved DimStyle. These -/// are read-only mirrors of the style's dimension variables, injected by the -/// panel after the entity's own Misc/Geometry groups. +/// Build the linear-dimension property groups from the assigned style plus +/// any entity-level DSTYLE overrides. Conditional rows remain visible but are +/// read-only while their controlling option is disabled. pub fn style_sections( style: &DimStyle, dimension: &Dimension, document: &CadDocument, -) -> Vec { - let s = style; - let yn = |b: bool| if b { "Yes" } else { "No" }; - let onoff = |b: bool| if b { "On" } else { "Off" }; - let f = |v: f64| format!("{v:.4}"); - let dsep = { - let c = s.dimdsep as u8 as char; - if s.dimdsep > 0 && !c.is_control() { - c.to_string() - } else { - s.dimdsep.to_string() - } +) -> Vec { + use crate::entities::dim_override as ov; + + let data = &dimension.base().common.extended_data; + let real = |code, inherited| ov::real(data, code).unwrap_or(inherited); + let int = |code, inherited| ov::int(data, code).unwrap_or(inherited); + let string = |code, inherited: &str| { + ov::string(data, code).unwrap_or_else(|| inherited.to_string()) }; - let tol_display = if s.dimlim { + let on = |value: bool| if value { "On" } else { "Off" }; + let yes = |value: bool| if value { "Yes" } else { "No" }; + let number = |label: &str, field: &'static str, value: f64, editable: bool| { + property( + label, + field, + if editable { + PropValue::EditText(format!("{value:.4}")) + } else { + PropValue::ReadOnly(format!("{value:.4}")) + }, + ) + }; + let text = |label: &str, field: &'static str, value: String, editable: bool| { + property( + label, + field, + if editable { + PropValue::PlainText(value) + } else { + PropValue::ReadOnly(value) + }, + ) + }; + let choice = |label: &str, + field: &'static str, + selected: &str, + options: &[&str], + editable: bool| { + property( + label, + field, + if editable { + choice_value(selected, options) + } else { + PropValue::ReadOnly(selected.to_string()) + }, + ) + }; + + let s = style; + let dimfxlon = int(ov::DIMFXLON, s.dimfxlon as i16) != 0; + let dimlunit = int(ov::DIMLUNIT, s.dimlunit); + let dimfrac = int(ov::DIMFRAC, s.dimfrac); + let dimalt = int(ov::DIMALT, s.dimalt as i16) != 0; + let dimtol = int(ov::DIMTOL, s.dimtol as i16) != 0; + let dimlim = int(ov::DIMLIM, s.dimlim as i16) != 0; + let dimtp = real(ov::DIMTP, s.dimtp); + let dimtm = real(ov::DIMTM, s.dimtm); + let dimgap = real(ov::DIMGAP, s.dimgap); + let tolerance_display = if dimgap < 0.0 { + "Basic" + } else if dimlim { "Limits" - } else if s.dimtol { + } else if dimtol && (dimtp - dimtm).abs() <= 1e-12 { + "Symmetrical" + } else if dimtol { "Deviation" } else { "None" }; + let tolerance_enabled = matches!( + tolerance_display, + "Symmetrical" | "Deviation" | "Limits" + ); + let alternate_tolerance_enabled = tolerance_enabled && dimalt; + let dimzin = int(ov::DIMZIN, s.dimzin); + let dimaltz = int(ov::DIMALTZ, s.dimaltz); + let dimtzin = int(ov::DIMTZIN, s.dimtzin); + let dimalttz = int(ov::DIMALTTZ, s.dimalttz); + let annotative = s.annotative + || !crate::scene::annotative::object_scale_memberships( + document, + dimension.base().common.handle, + ) + .is_empty(); - let mut sections = vec![ - PropSection { - title: t!("Lines & Arrows").into_owned(), - props: vec![ - ro( - t!("Arrow 1").as_ref(), - "dim_arrowhead_1", - block_name(document, s.dimblk1, &s.dimblk1_name), - ), - ro( - t!("Arrow 2").as_ref(), - "dim_arrowhead_2", - block_name(document, s.dimblk2, &s.dimblk2_name), - ), - ro(t!("Arrow size").as_ref(), "dim_arrow_size", f(s.dimasz)), - ro( - t!("Dim line lineweight").as_ref(), - "dim_line_lineweight", - s.dimlwd.to_string(), - ), - ro( - t!("Ext line lineweight").as_ref(), - "dim_ext_line_lineweight", - s.dimlwe.to_string(), - ), - ro(t!("Dim line 1").as_ref(), "dim_line_1", onoff(!s.dimsd1)), - ro(t!("Dim line 2").as_ref(), "dim_line_2", onoff(!s.dimsd2)), - ro(t!("Dim line color").as_ref(), "dim_line_color", s.dimclrd.to_string()), - ro( - t!("Dim line linetype").as_ref(), - "dim_linetype", - linetype_name(document, s.dimltex_handle), - ), - ro(t!("Dim line ext").as_ref(), "dim_line_ext", f(s.dimdle)), - ro( - t!("Ext line 1 linetype").as_ref(), - "dim_ext_linetype_1", - linetype_name(document, s.dimltex1_handle), - ), - ro( - t!("Ext line 2 linetype").as_ref(), - "dim_ext_linetype_2", - linetype_name(document, s.dimltex2_handle), - ), - ro(t!("Ext line 1").as_ref(), "dim_ext_line_1", onoff(!s.dimse1)), - ro(t!("Ext line 2").as_ref(), "dim_ext_line_2", onoff(!s.dimse2)), - ro(t!("Ext line fixed").as_ref(), "dim_ext_line_fixed", yn(s.dimfxlon)), - ro( - t!("Ext line fixed length").as_ref(), - "dim_ext_line_fixed_length", - f(s.dimfxl), - ), - ro(t!("Ext line color").as_ref(), "dim_ext_line_color", s.dimclre.to_string()), - ro(t!("Ext line ext").as_ref(), "dim_ext_line_ext", f(s.dimexe)), - ro(t!("Ext line offset").as_ref(), "dim_ext_line_offset", f(s.dimexo)), - ], - }, - PropSection { - title: t!("Text").into_owned(), - props: vec![ - ro(t!("Fill color").as_ref(), "dim_text_fill_color", s.dimtfillclr.to_string()), - ro(t!("Text color").as_ref(), "dim_text_color", s.dimclrt.to_string()), - ro(t!("Text height").as_ref(), "dim_text_height", f(s.dimtxt)), - ro(t!("Text offset").as_ref(), "dim_text_offset", f(s.dimgap)), - ro(t!("Text pos vert").as_ref(), "dim_text_pos_vert", s.dimtad.to_string()), - ro(t!("Text pos hor").as_ref(), "dim_text_pos_hor", s.dimjust.to_string()), - ro(t!("Text outside align").as_ref(), "dim_text_outside_align", yn(s.dimtoh)), - ro(t!("Text inside align").as_ref(), "dim_text_inside_align", yn(s.dimtih)), - ro(t!("Text style").as_ref(), "dim_text_style", s.dimtxsty.clone()), - ], - }, - PropSection { - title: t!("Fit").into_owned(), - props: vec![ - ro(t!("Fit").as_ref(), "dim_fit", s.dimatfit.to_string()), - ro(t!("Text inside").as_ref(), "dim_text_inside", yn(s.dimtix)), - ro(t!("Text movement").as_ref(), "dim_text_movement", s.dimtmove.to_string()), - ro(t!("Dim scale overall").as_ref(), "dim_scale_overall", f(s.dimscale)), - ro(t!("Dim line forced").as_ref(), "dim_line_forced", yn(s.dimtofl)), - ro(t!("Dim line inside").as_ref(), "dim_line_inside", yn(s.dimsoxd)), - ], - }, - PropSection { - title: t!("Primary Units").into_owned(), - props: vec![ - ro(t!("Dim units").as_ref(), "dim_units", s.dimlunit.to_string()), - ro(t!("Precision").as_ref(), "dim_precision", s.dimdec.to_string()), - ro(t!("Decimal separator").as_ref(), "dim_decimal_separator", dsep), - ro(t!("Dim prefix/suffix").as_ref(), "dim_prefix_suffix", s.dimpost.clone()), - ro(t!("Dim roundoff").as_ref(), "dim_roundoff", f(s.dimrnd)), - ro( - t!("Suppress leading zeros").as_ref(), - "dim_suppress_leading_zeros", - yn(s.dimzin & 4 != 0), - ), - ro( - t!("Suppress trailing zeros").as_ref(), - "dim_suppress_trailing_zeros", - yn(s.dimzin & 8 != 0), - ), - ro(t!("Dim scale linear").as_ref(), "dim_scale_linear", f(s.dimlfac)), - ro(t!("Angle format").as_ref(), "dim_angle_format", s.dimaunit.to_string()), - ro(t!("Angle precision").as_ref(), "dim_angle_precision", s.dimadec.to_string()), - ], - }, - PropSection { - title: t!("Alternate Units").into_owned(), - props: vec![ - ro(t!("Alt enabled").as_ref(), "dim_alt_enabled", yn(s.dimalt)), - ro(t!("Alt format").as_ref(), "dim_alt_format", s.dimaltu.to_string()), - ro(t!("Alt precision").as_ref(), "dim_alt_precision", s.dimaltd.to_string()), - ro(t!("Alt scale factor").as_ref(), "dim_alt_scale_factor", f(s.dimaltf)), - ro(t!("Alt roundoff").as_ref(), "dim_alt_roundoff", f(s.dimaltrnd)), - ro(t!("Alt prefix/suffix").as_ref(), "dim_alt_prefix_suffix", s.dimapost.clone()), - ro( - t!("Alt suppress leading zeros").as_ref(), - "dim_alt_suppress_leading_zeros", - yn(s.dimaltz & 4 != 0), - ), - ro( - t!("Alt suppress trailing zeros").as_ref(), - "dim_alt_suppress_trailing_zeros", - yn(s.dimaltz & 8 != 0), - ), - ], - }, - PropSection { - title: t!("Tolerances").into_owned(), - props: vec![ - ro(t!("Tolerance display").as_ref(), "dim_tolerance_display", tol_display), - ro(t!("Tolerance limit lower").as_ref(), "dim_tolerance_limit_lower", f(s.dimtm)), - ro(t!("Tolerance limit upper").as_ref(), "dim_tolerance_limit_upper", f(s.dimtp)), - ro( - t!("Tolerance precision").as_ref(), - "dim_tolerance_precision", - s.dimtdec.to_string(), - ), - ro( - t!("Tolerance text height").as_ref(), - "dim_tolerance_text_height", - f(s.dimtfac), - ), - ro( - t!("Tolerance pos vert").as_ref(), - "dim_tolerance_pos_vert", - s.dimtolj.to_string(), - ), - ro( - t!("Tolerance suppress leading zeros").as_ref(), - "dim_tolerance_suppress_leading_zeros", - yn(s.dimtzin & 4 != 0), - ), - ro( - t!("Tolerance suppress trailing zeros").as_ref(), - "dim_tolerance_suppress_trailing_zeros", - yn(s.dimtzin & 8 != 0), - ), - ], - }, - ]; + let overridden_text_style = ov::handle(data, ov::DIMTXSTY); + let text_style_name = overridden_text_style + .and_then(|handle| { + document + .text_styles + .iter() + .find(|record| record.handle == handle) + .map(|record| record.name.clone()) + }) + .unwrap_or_else(|| s.dimtxsty.clone()); + let text_height_editable = document + .text_styles + .iter() + .find(|record| { + overridden_text_style + .is_some_and(|handle| record.handle == handle) + || (overridden_text_style.is_none() + && record.name.eq_ignore_ascii_case(&text_style_name)) + }) + .is_none_or(|record| !record.has_fixed_height()); - let data = &dimension.base().common.extended_data; - let yes_no = |value: bool| if value { "Yes" } else { "No" }.to_string(); - for property in sections - .iter_mut() - .flat_map(|section| section.props.iter_mut()) - { - let inherited = match &property.value { - crate::scene::model::object::PropValue::ReadOnly(value) => value.clone(), - _ => continue, - }; - if let Some(code) = crate::entities::dim_override::property_real_code(property.field) { - let value = crate::entities::dim_override::real(data, code) - .map(|number| number.to_string()) - .unwrap_or(inherited); - property.value = crate::scene::model::object::PropValue::EditText(value); - continue; + let (dim_prefix, dim_suffix) = + split_measurement_template(&string(ov::DIMPOST, &s.dimpost)); + let (alt_prefix, alt_suffix) = + split_measurement_template(&string(ov::DIMAPOST, &s.dimapost)); + let decimal_separator = { + let value = int(ov::DIMDSEP, s.dimdsep); + let character = value as u8 as char; + if value > 0 && !character.is_control() { + character.to_string() + } else { + value.to_string() } - if let Some(code) = crate::entities::dim_override::property_string_code(property.field) { - let value = crate::entities::dim_override::string(data, code).unwrap_or(inherited); - property.value = crate::scene::model::object::PropValue::PlainText(value); - continue; - } - if let Some(code) = crate::entities::dim_override::property_int_code(property.field) { - let number = crate::entities::dim_override::int(data, code); - let value = number.map(|number| number.to_string()).unwrap_or(inherited); - property.value = match property.field { - "dim_ext_line_fixed" - | "dim_text_outside_align" - | "dim_text_inside_align" - | "dim_text_inside" - | "dim_line_forced" - | "dim_line_inside" - | "dim_alt_enabled" => crate::scene::model::object::PropValue::Choice { - selected: number.map(|number| yes_no(number != 0)).unwrap_or(value), - options: vec!["Yes".to_string(), "No".to_string()], - }, - "dim_units" => crate::scene::model::object::PropValue::Choice { - selected: linear_unit_label(number.unwrap_or(s.dimlunit)).to_string(), - options: vec![ - "Scientific", "Decimal", "Engineering", "Architectural", - "Fractional", "Desktop", - ] - .into_iter() - .map(str::to_string) - .collect(), - }, - "dim_angle_format" => crate::scene::model::object::PropValue::Choice { - selected: angle_unit_label(number.unwrap_or(s.dimaunit)).to_string(), - options: vec![ - "Decimal degrees", "Degrees/minutes/seconds", "Gradians", - "Radians", "Surveyors units", - ] - .into_iter() - .map(str::to_string) - .collect(), - }, - "dim_text_pos_vert" => choice_value( - match number.unwrap_or(s.dimtad) { - 1 => "Above", - 2 => "Outside", - 3 => "JIS", - 4 => "Below", - _ => "Centered", - }, - &["Centered", "Above", "Outside", "JIS", "Below"], - ), - "dim_text_pos_hor" => choice_value( - match number.unwrap_or(s.dimjust) { - 1 => "At extension line 1", - 2 => "At extension line 2", - 3 => "Over extension line 1", - 4 => "Over extension line 2", - _ => "Centered", - }, - &[ - "Centered", "At extension line 1", "At extension line 2", - "Over extension line 1", "Over extension line 2", - ], - ), - "dim_fit" => choice_value( - match number.unwrap_or(s.dimatfit) { - 1 => "Arrows", - 2 => "Text", - 3 => "Both text and arrows", - 4 => "Always keep text", - _ => "Either text or arrows", - }, - &[ - "Either text or arrows", "Arrows", "Text", - "Both text and arrows", "Always keep text", - ], - ), - "dim_text_movement" => choice_value( - match number.unwrap_or(s.dimtmove) { - 1 => "Add leader", - 2 => "Move text freely", - _ => "Move dimension line", - }, - &["Move dimension line", "Add leader", "Move text freely"], - ), - "dim_tolerance_pos_vert" => choice_value( - match number.unwrap_or(s.dimtolj) { - 1 => "Middle", - 2 => "Top", - _ => "Bottom", - }, - &["Bottom", "Middle", "Top"], - ), - "dim_line_lineweight" | "dim_ext_line_lineweight" => { - let inherited = if property.field == "dim_line_lineweight" { - s.dimlwd - } else { - s.dimlwe - }; - crate::scene::model::object::PropValue::Choice { - selected: lineweight_label(number.unwrap_or(inherited)), - options: lineweight_options(), - } - } - _ => crate::scene::model::object::PropValue::EditText(value), - }; - } - } - - let integer_or = |code, inherited| { - crate::entities::dim_override::int(data, code).unwrap_or(inherited) }; - for property in sections - .iter_mut() - .flat_map(|section| section.props.iter_mut()) - { - let selected = match property.field { - "dim_line_1" => Some(if integer_or(crate::entities::dim_override::DIMSD1, s.dimsd1 as i16) == 0 { "On" } else { "Off" }), - "dim_line_2" => Some(if integer_or(crate::entities::dim_override::DIMSD2, s.dimsd2 as i16) == 0 { "On" } else { "Off" }), - "dim_ext_line_1" => Some(if integer_or(crate::entities::dim_override::DIMSE1, s.dimse1 as i16) == 0 { "On" } else { "Off" }), - "dim_ext_line_2" => Some(if integer_or(crate::entities::dim_override::DIMSE2, s.dimse2 as i16) == 0 { "On" } else { "Off" }), - "dim_suppress_leading_zeros" => Some(if integer_or(crate::entities::dim_override::DIMZIN, s.dimzin) & 4 != 0 { "Yes" } else { "No" }), - "dim_suppress_trailing_zeros" => Some(if integer_or(crate::entities::dim_override::DIMZIN, s.dimzin) & 8 != 0 { "Yes" } else { "No" }), - "dim_alt_suppress_leading_zeros" => Some(if integer_or(crate::entities::dim_override::DIMALTZ, s.dimaltz) & 4 != 0 { "Yes" } else { "No" }), - "dim_alt_suppress_trailing_zeros" => Some(if integer_or(crate::entities::dim_override::DIMALTZ, s.dimaltz) & 8 != 0 { "Yes" } else { "No" }), - "dim_tolerance_suppress_leading_zeros" => Some(if integer_or(crate::entities::dim_override::DIMTZIN, s.dimtzin) & 4 != 0 { "Yes" } else { "No" }), - "dim_tolerance_suppress_trailing_zeros" => Some(if integer_or(crate::entities::dim_override::DIMTZIN, s.dimtzin) & 8 != 0 { "Yes" } else { "No" }), - _ => None, - }; - if let Some(selected) = selected { - let options = if matches!( - property.field, - "dim_line_1" | "dim_line_2" | "dim_ext_line_1" | "dim_ext_line_2" - ) { - &["On", "Off"][..] - } else { - &["Yes", "No"][..] - }; - property.value = choice_value(selected, options); - } - if property.field == "dim_tolerance_display" { - let limits = integer_or(crate::entities::dim_override::DIMLIM, s.dimlim as i16) != 0; - let tolerance = integer_or(crate::entities::dim_override::DIMTOL, s.dimtol as i16) != 0; - let selected = if limits { "Limits" } else if tolerance { "Deviation" } else { "None" }; - property.value = choice_value(selected, &["None", "Deviation", "Limits"]); - } - } - let arrow_options: Vec = std::iter::once("Closed filled".to_string()) + let mut arrow_options: Vec = std::iter::once("Closed filled".to_string()) .chain( document .block_records @@ -1791,7 +1549,7 @@ pub fn style_sections( .filter(|name| !name.is_empty()), ) .collect(); - let linetype_options: Vec = document + let mut linetype_options: Vec = document .line_types .iter() .map(|line_type| line_type.name.clone()) @@ -1800,65 +1558,790 @@ pub fn style_sections( let text_style_options: Vec = document .text_styles .iter() - .map(|style| style.name.clone()) + .map(|record| record.name.clone()) .filter(|name| !name.is_empty()) .collect(); - for property in sections - .iter_mut() - .flat_map(|section| section.props.iter_mut()) - { - let mut current = match &property.value { - crate::scene::model::object::PropValue::ReadOnly(value) => value.clone(), - _ => continue, - }; - if matches!(property.field, "dim_arrowhead_1" | "dim_arrowhead_2") { - let code = if property.field == "dim_arrowhead_1" { - crate::entities::dim_override::DIMBLK1 - } else { - crate::entities::dim_override::DIMBLK2 - }; - if let Some(handle) = crate::entities::dim_override::handle(data, code) { - current = block_name(document, handle, ""); - } - property.value = crate::scene::model::object::PropValue::Choice { - selected: current, - options: arrow_options.clone(), - }; - } else if matches!( - property.field, - "dim_linetype" | "dim_ext_linetype_1" | "dim_ext_linetype_2" - ) { - let code = match property.field { - "dim_linetype" => crate::entities::dim_override::DIMLTYPE, - "dim_ext_linetype_1" => crate::entities::dim_override::DIMLTEX1, - _ => crate::entities::dim_override::DIMLTEX2, - }; - if let Some(handle) = crate::entities::dim_override::handle(data, code) { - current = linetype_name(document, handle); - } - property.value = crate::scene::model::object::PropValue::Choice { - selected: current, - options: linetype_options.clone(), - }; - } else if property.field == "dim_text_style" { - if let Some(handle) = crate::entities::dim_override::handle( - data, - crate::entities::dim_override::DIMTXSTY, - ) { - current = document - .text_styles - .iter() - .find(|style| style.handle == handle) - .map(|style| style.name.clone()) - .unwrap_or(current); - } - property.value = crate::scene::model::object::PropValue::Choice { - selected: current, - options: text_style_options.clone(), - }; + + let arrow_name = |code, inherited_handle, inherited_name: &str| { + ov::handle(data, code) + .map(|handle| block_name(document, handle, inherited_name)) + .unwrap_or_else(|| block_name(document, inherited_handle, inherited_name)) + }; + let linetype = |code, inherited| { + linetype_name(document, ov::handle(data, code).unwrap_or(inherited)) + }; + let arrow_1 = arrow_name(ov::DIMBLK1, s.dimblk1, &s.dimblk1_name); + let arrow_2 = arrow_name(ov::DIMBLK2, s.dimblk2, &s.dimblk2_name); + for current in [&arrow_1, &arrow_2] { + if !arrow_options.contains(current) { + arrow_options.push(current.clone()); } } - sections + for current in [ + linetype(ov::DIMLTYPE, s.dimltex_handle), + linetype(ov::DIMLTEX1, s.dimltex1_handle), + linetype(ov::DIMLTEX2, s.dimltex2_handle), + ] { + if !linetype_options.contains(¤t) { + linetype_options.push(current); + } + } + + let precision_options: Vec = (0..=8).map(|value| value.to_string()).collect(); + let linear_units = [ + "Scientific", + "Decimal", + "Engineering", + "Architectural", + "Fractional", + "Desktop", + ]; + let alternate_unit_options = [ + "Scientific", + "Decimal", + "Engineering", + "Architectural stacked", + "Fractional stacked", + "Architectural", + "Fractional", + "Desktop", + ]; + + let fill_mode = int(ov::DIMTFILL, s.dimtfill); + let fill_color = ov::color(data, ov::DIMTFILLCLR) + .unwrap_or_else(|| AcadColor::from_index(s.dimtfillclr)); + let fill_value = match fill_mode { + 1 => choice_value("Background", &["None", "Background", "Color"]), + 2 => PropValue::ColorChoice(fill_color), + _ => choice_value("None", &["None", "Background", "Color"]), + }; + + vec![ + PropSection { + title: t!("Lines & Arrows").into_owned(), + props: vec![ + property( + t!("Arrow 1").as_ref(), + "dim_arrowhead_1", + PropValue::Choice { + selected: arrow_1, + options: arrow_options.clone(), + }, + ), + property( + t!("Arrow 2").as_ref(), + "dim_arrowhead_2", + PropValue::Choice { + selected: arrow_2, + options: arrow_options, + }, + ), + number( + t!("Arrow size").as_ref(), + "dim_arrow_size", + real(ov::DIMASZ, s.dimasz), + true, + ), + property( + t!("Dim line lineweight").as_ref(), + "dim_line_lineweight", + PropValue::Choice { + selected: lineweight_label(int(ov::DIMLWD, s.dimlwd)), + options: lineweight_options(), + }, + ), + property( + t!("Ext line lineweight").as_ref(), + "dim_ext_line_lineweight", + PropValue::Choice { + selected: lineweight_label(int(ov::DIMLWE, s.dimlwe)), + options: lineweight_options(), + }, + ), + choice( + t!("Dim line 1").as_ref(), + "dim_line_1", + on(int(ov::DIMSD1, s.dimsd1 as i16) == 0), + &["On", "Off"], + true, + ), + choice( + t!("Dim line 2").as_ref(), + "dim_line_2", + on(int(ov::DIMSD2, s.dimsd2 as i16) == 0), + &["On", "Off"], + true, + ), + property( + t!("Dim line color").as_ref(), + "dim_line_color", + PropValue::ColorChoice( + ov::color(data, ov::DIMCLRD) + .unwrap_or_else(|| AcadColor::from_index(s.dimclrd)), + ), + ), + property( + t!("Dim line linetype").as_ref(), + "dim_linetype", + PropValue::Choice { + selected: linetype(ov::DIMLTYPE, s.dimltex_handle), + options: linetype_options.clone(), + }, + ), + number( + t!("Dim line ext").as_ref(), + "dim_line_ext", + real(ov::DIMDLE, s.dimdle), + true, + ), + property( + t!("Ext line 1 linetype").as_ref(), + "dim_ext_linetype_1", + PropValue::Choice { + selected: linetype(ov::DIMLTEX1, s.dimltex1_handle), + options: linetype_options.clone(), + }, + ), + property( + t!("Ext line 2 linetype").as_ref(), + "dim_ext_linetype_2", + PropValue::Choice { + selected: linetype(ov::DIMLTEX2, s.dimltex2_handle), + options: linetype_options, + }, + ), + choice( + t!("Ext line 1").as_ref(), + "dim_ext_line_1", + on(int(ov::DIMSE1, s.dimse1 as i16) == 0), + &["On", "Off"], + true, + ), + choice( + t!("Ext line 2").as_ref(), + "dim_ext_line_2", + on(int(ov::DIMSE2, s.dimse2 as i16) == 0), + &["On", "Off"], + true, + ), + choice( + t!("Ext line fixed").as_ref(), + "dim_ext_line_fixed", + on(dimfxlon), + &["On", "Off"], + true, + ), + number( + t!("Ext line fixed length").as_ref(), + "dim_ext_line_fixed_length", + real(ov::DIMFXL, s.dimfxl), + dimfxlon, + ), + property( + t!("Ext line color").as_ref(), + "dim_ext_line_color", + PropValue::ColorChoice( + ov::color(data, ov::DIMCLRE) + .unwrap_or_else(|| AcadColor::from_index(s.dimclre)), + ), + ), + number( + t!("Ext line ext").as_ref(), + "dim_ext_line_ext", + real(ov::DIMEXE, s.dimexe), + true, + ), + number( + t!("Ext line offset").as_ref(), + "dim_ext_line_offset", + real(ov::DIMEXO, s.dimexo), + true, + ), + ], + }, + PropSection { + title: t!("Text").into_owned(), + props: vec![ + property( + t!("Fill color").as_ref(), + "dim_text_fill_color", + fill_value, + ), + choice( + t!("Fractional type").as_ref(), + "dim_fractional_type", + fraction_type_label(dimfrac), + &["Horizontal", "Diagonal", "Not stacked"], + matches!(dimlunit, 4 | 5), + ), + property( + t!("Text color").as_ref(), + "dim_text_color", + PropValue::ColorChoice( + ov::color(data, ov::DIMCLRT) + .unwrap_or_else(|| AcadColor::from_index(s.dimclrt)), + ), + ), + number( + t!("Text height").as_ref(), + "dim_text_height", + real(ov::DIMTXT, s.dimtxt), + text_height_editable, + ), + number( + t!("Text offset").as_ref(), + "dim_text_offset", + dimgap.abs(), + true, + ), + choice( + t!("Text outside align").as_ref(), + "dim_text_outside_align", + on(int(ov::DIMTOH, s.dimtoh as i16) != 0), + &["On", "Off"], + true, + ), + choice( + t!("Text pos hor").as_ref(), + "dim_text_pos_hor", + text_horizontal_label(int(ov::DIMJUST, s.dimjust)), + &[ + "Centered", + "At extension line 1", + "At extension line 2", + "Over extension line 1", + "Over extension line 2", + ], + true, + ), + choice( + t!("Text pos vert").as_ref(), + "dim_text_pos_vert", + text_vertical_label(int(ov::DIMTAD, s.dimtad)), + &["Centered", "Above", "Outside", "JIS", "Below"], + true, + ), + property( + t!("Text style").as_ref(), + "dim_text_style", + PropValue::Choice { + selected: text_style_name, + options: text_style_options, + }, + ), + choice( + t!("Text inside align").as_ref(), + "dim_text_inside_align", + on(int(ov::DIMTIH, s.dimtih as i16) != 0), + &["On", "Off"], + true, + ), + property( + t!("Text position X").as_ref(), + "text_x", + PropValue::EditText(format!( + "{:.4}", + dimension.base().text_middle_point.x + )), + ), + property( + t!("Text position Y").as_ref(), + "text_y", + PropValue::EditText(format!( + "{:.4}", + dimension.base().text_middle_point.y + )), + ), + property( + t!("Text rotation").as_ref(), + "text_rotation", + PropValue::EditText(format!( + "{:.4}", + dimension.base().text_rotation.to_degrees() + )), + ), + choice( + t!("Text view direction").as_ref(), + "dim_text_view_direction", + if int(ov::DIMTXTDIRECTION, s.dimtxtdirection as i16) != 0 { + "Right-to-left" + } else { + "Left-to-right" + }, + &["Left-to-right", "Right-to-left"], + true, + ), + property( + t!("Measurement").as_ref(), + "measurement", + PropValue::ReadOnly(format!("{:.4}", dimension.measurement())), + ), + property( + t!("Text override").as_ref(), + "text_override", + PropValue::PlainText( + dimension.base().text_override().unwrap_or("").to_string(), + ), + ), + ], + }, + PropSection { + title: t!("Fit").into_owned(), + props: vec![ + choice( + t!("Fit").as_ref(), + "dim_fit", + fit_label(int(ov::DIMATFIT, s.dimatfit)), + &["Both text and arrows", "Arrows", "Text", "Best fit"], + true, + ), + choice( + t!("Text inside").as_ref(), + "dim_text_inside", + on(int(ov::DIMTIX, s.dimtix as i16) != 0), + &["On", "Off"], + true, + ), + choice( + t!("Text movement").as_ref(), + "dim_text_movement", + text_movement_label(int(ov::DIMTMOVE, s.dimtmove)), + &[ + "Keep dim line with text", + "Move text, add leader", + "Move text, no leader", + ], + true, + ), + number( + t!("Dim scale overall").as_ref(), + "dim_scale_overall", + real(ov::DIMSCALE, s.dimscale), + !annotative, + ), + choice( + t!("Dim line forced").as_ref(), + "dim_line_forced", + on(int(ov::DIMTOFL, s.dimtofl as i16) != 0), + &["On", "Off"], + true, + ), + choice( + t!("Dim line inside").as_ref(), + "dim_line_inside", + on(int(ov::DIMSOXD, s.dimsoxd as i16) != 0), + &["On", "Off"], + true, + ), + ], + }, + PropSection { + title: t!("Primary Units").into_owned(), + props: vec![ + text( + t!("Decimal separator").as_ref(), + "dim_decimal_separator", + decimal_separator, + true, + ), + text( + t!("Dim prefix").as_ref(), + "dim_prefix", + dim_prefix, + true, + ), + text( + t!("Dim suffix").as_ref(), + "dim_suffix", + dim_suffix, + true, + ), + text( + t!("Dim sub-units suffix").as_ref(), + "dim_sub_units_suffix", + string(ov::DIMMZS, &s.dimmzs), + dimzin & 4 != 0, + ), + number( + t!("Dim roundoff").as_ref(), + "dim_roundoff", + real(ov::DIMRND, s.dimrnd), + true, + ), + number( + t!("Dim scale linear").as_ref(), + "dim_scale_linear", + real(ov::DIMLFAC, s.dimlfac), + true, + ), + number( + t!("Dim sub-units scale").as_ref(), + "dim_sub_units_scale", + real(ov::DIMMZF, s.dimmzf), + dimzin & 4 != 0, + ), + choice( + t!("Dim units").as_ref(), + "dim_units", + linear_unit_label(dimlunit), + &linear_units, + true, + ), + choice( + t!("Suppress leading zeros").as_ref(), + "dim_suppress_leading_zeros", + yes(dimzin & 4 != 0), + &["Yes", "No"], + true, + ), + choice( + t!("Suppress trailing zeros").as_ref(), + "dim_suppress_trailing_zeros", + yes(dimzin & 8 != 0), + &["Yes", "No"], + true, + ), + choice( + t!("Suppress zero feet").as_ref(), + "dim_suppress_zero_feet", + yes(suppresses_zero_feet(dimzin)), + &["Yes", "No"], + true, + ), + choice( + t!("Suppress zero inches").as_ref(), + "dim_suppress_zero_inches", + yes(suppresses_zero_inches(dimzin)), + &["Yes", "No"], + true, + ), + property( + t!("Precision").as_ref(), + "dim_precision", + PropValue::Choice { + selected: int(ov::DIMDEC, s.dimdec).to_string(), + options: precision_options.clone(), + }, + ), + ], + }, + PropSection { + title: t!("Alternate Units").into_owned(), + props: vec![ + choice( + t!("Alt enabled").as_ref(), + "dim_alt_enabled", + on(dimalt), + &["On", "Off"], + true, + ), + choice( + t!("Alt format").as_ref(), + "dim_alt_format", + alternate_unit_label(int(ov::DIMALTU, s.dimaltu)), + &alternate_unit_options, + dimalt, + ), + property( + t!("Alt precision").as_ref(), + "dim_alt_precision", + if dimalt { + PropValue::Choice { + selected: int(ov::DIMALTD, s.dimaltd).to_string(), + options: precision_options.clone(), + } + } else { + PropValue::ReadOnly(int(ov::DIMALTD, s.dimaltd).to_string()) + }, + ), + number( + t!("Alt scale factor").as_ref(), + "dim_alt_scale_factor", + real(ov::DIMALTF, s.dimaltf), + dimalt, + ), + number( + t!("Alt sub-units scale").as_ref(), + "dim_alt_sub_units_scale", + real(ov::DIMALTMZF, s.dimaltmzf), + dimalt && dimaltz & 4 != 0, + ), + number( + t!("Alt roundoff").as_ref(), + "dim_alt_roundoff", + real(ov::DIMALTRND, s.dimaltrnd), + dimalt, + ), + text( + t!("Alt prefix").as_ref(), + "dim_alt_prefix", + alt_prefix, + dimalt, + ), + text( + t!("Alt suffix").as_ref(), + "dim_alt_suffix", + alt_suffix, + dimalt, + ), + text( + t!("Alt sub-units suffix").as_ref(), + "dim_alt_sub_units_suffix", + string(ov::DIMALTMZS, &s.dimaltmzs), + dimalt && dimaltz & 4 != 0, + ), + choice( + t!("Alt suppress leading zeros").as_ref(), + "dim_alt_suppress_leading_zeros", + yes(dimaltz & 4 != 0), + &["Yes", "No"], + dimalt, + ), + choice( + t!("Alt suppress trailing zeros").as_ref(), + "dim_alt_suppress_trailing_zeros", + yes(dimaltz & 8 != 0), + &["Yes", "No"], + dimalt, + ), + choice( + t!("Alt suppress zero feet").as_ref(), + "dim_alt_suppress_zero_feet", + yes(suppresses_zero_feet(dimaltz)), + &["Yes", "No"], + dimalt, + ), + choice( + t!("Alt suppress zero inches").as_ref(), + "dim_alt_suppress_zero_inches", + yes(suppresses_zero_inches(dimaltz)), + &["Yes", "No"], + dimalt, + ), + ], + }, + PropSection { + title: t!("Tolerances").into_owned(), + props: vec![ + choice( + t!("Tolerance display").as_ref(), + "dim_tolerance_display", + tolerance_display, + &["None", "Symmetrical", "Deviation", "Limits", "Basic"], + true, + ), + property( + t!("Tolerance precision").as_ref(), + "dim_tolerance_precision", + if tolerance_enabled { + PropValue::Choice { + selected: int(ov::DIMTDEC, s.dimtdec).to_string(), + options: precision_options.clone(), + } + } else { + PropValue::ReadOnly(int(ov::DIMTDEC, s.dimtdec).to_string()) + }, + ), + number( + t!("Tolerance limit lower").as_ref(), + "dim_tolerance_limit_lower", + dimtm, + matches!(tolerance_display, "Deviation" | "Limits"), + ), + number( + t!("Tolerance limit upper").as_ref(), + "dim_tolerance_limit_upper", + dimtp, + tolerance_enabled, + ), + number( + t!("Tolerance text height").as_ref(), + "dim_tolerance_text_height", + real(ov::DIMTFAC, s.dimtfac), + tolerance_enabled, + ), + choice( + t!("Tolerance pos vert").as_ref(), + "dim_tolerance_pos_vert", + tolerance_vertical_label(int(ov::DIMTOLJ, s.dimtolj)), + &["Bottom", "Middle", "Top"], + tolerance_enabled, + ), + choice( + t!("Tolerance alignment").as_ref(), + "dim_tolerance_alignment", + tolerance_alignment_label(int(ov::DIMTALN, 0)), + &["Align decimal separators", "Align operational symbols"], + tolerance_display == "Deviation", + ), + choice( + t!("Tolerance suppress leading zeros").as_ref(), + "dim_tolerance_suppress_leading_zeros", + yes(dimtzin & 4 != 0), + &["Yes", "No"], + tolerance_enabled, + ), + choice( + t!("Tolerance suppress trailing zeros").as_ref(), + "dim_tolerance_suppress_trailing_zeros", + yes(dimtzin & 8 != 0), + &["Yes", "No"], + tolerance_enabled, + ), + choice( + t!("Tolerance suppress zero feet").as_ref(), + "dim_tolerance_suppress_zero_feet", + yes(suppresses_zero_feet(dimtzin)), + &["Yes", "No"], + tolerance_enabled, + ), + choice( + t!("Tolerance suppress zero inches").as_ref(), + "dim_tolerance_suppress_zero_inches", + yes(suppresses_zero_inches(dimtzin)), + &["Yes", "No"], + tolerance_enabled, + ), + property( + t!("Alt tolerance precision").as_ref(), + "dim_alt_tolerance_precision", + if alternate_tolerance_enabled { + PropValue::Choice { + selected: int(ov::DIMALTTD, s.dimalttd).to_string(), + options: precision_options, + } + } else { + PropValue::ReadOnly(int(ov::DIMALTTD, s.dimalttd).to_string()) + }, + ), + choice( + t!("Alt tolerance suppress leading zeros").as_ref(), + "dim_alt_tolerance_suppress_leading_zeros", + yes(dimalttz & 4 != 0), + &["Yes", "No"], + alternate_tolerance_enabled, + ), + choice( + t!("Alt tolerance suppress trailing zeros").as_ref(), + "dim_alt_tolerance_suppress_trailing_zeros", + yes(dimalttz & 8 != 0), + &["Yes", "No"], + alternate_tolerance_enabled, + ), + choice( + t!("Alt tolerance suppress zero feet").as_ref(), + "dim_alt_tolerance_suppress_zero_feet", + yes(suppresses_zero_feet(dimalttz)), + &["Yes", "No"], + alternate_tolerance_enabled, + ), + choice( + t!("Alt tolerance suppress zero inches").as_ref(), + "dim_alt_tolerance_suppress_zero_inches", + yes(suppresses_zero_inches(dimalttz)), + &["Yes", "No"], + alternate_tolerance_enabled, + ), + ], + }, + ] +} + +fn property(label: &str, field: &'static str, value: PropValue) -> Property { + Property { + label: label.to_string(), + field, + value, + } +} + +fn split_measurement_template(value: &str) -> (String, String) { + value + .split_once("<>") + .map(|(prefix, suffix)| (prefix.to_string(), suffix.to_string())) + .unwrap_or_else(|| (String::new(), value.to_string())) +} + +fn fraction_type_label(value: i16) -> &'static str { + match value { + 1 => "Diagonal", + 2 => "Not stacked", + _ => "Horizontal", + } +} + +fn suppresses_zero_feet(value: i16) -> bool { + matches!(value & 3, 0 | 3) +} + +fn suppresses_zero_inches(value: i16) -> bool { + matches!(value & 3, 0 | 2) +} + +fn text_horizontal_label(value: i16) -> &'static str { + match value { + 1 => "At extension line 1", + 2 => "At extension line 2", + 3 => "Over extension line 1", + 4 => "Over extension line 2", + _ => "Centered", + } +} + +fn text_vertical_label(value: i16) -> &'static str { + match value { + 1 => "Above", + 2 => "Outside", + 3 => "JIS", + 4 => "Below", + _ => "Centered", + } +} + +fn fit_label(value: i16) -> &'static str { + match value { + 1 => "Arrows", + 2 => "Text", + 3 => "Best fit", + _ => "Both text and arrows", + } +} + +fn text_movement_label(value: i16) -> &'static str { + match value { + 1 => "Move text, add leader", + 2 => "Move text, no leader", + _ => "Keep dim line with text", + } +} + +fn alternate_unit_label(value: i16) -> &'static str { + match value { + 1 => "Scientific", + 3 => "Engineering", + 4 => "Architectural stacked", + 5 => "Fractional stacked", + 6 => "Architectural", + 7 => "Fractional", + 8 => "Desktop", + _ => "Decimal", + } +} + +fn tolerance_vertical_label(value: i16) -> &'static str { + match value { + 0 => "Bottom", + 2 => "Top", + _ => "Middle", + } +} + +fn tolerance_alignment_label(value: i16) -> &'static str { + if value == 0 { + "Align decimal separators" + } else { + "Align operational symbols" + } } fn choice_value( @@ -1873,11 +2356,7 @@ fn choice_value( fn block_name(document: &CadDocument, handle: acadrust::Handle, fallback: &str) -> String { if handle.is_null() { - return if fallback.is_empty() { - "Closed filled".to_string() - } else { - fallback.to_string() - }; + return "Closed filled".to_string(); } document .block_records @@ -1907,15 +2386,6 @@ fn linear_unit_label(value: i16) -> &'static str { } } -fn angle_unit_label(value: i16) -> &'static str { - match value { - 1 => "Degrees/minutes/seconds", - 2 => "Gradians", - 3 => "Radians", - 4 => "Surveyors units", - _ => "Decimal degrees", - } -} use acadrust::{CadDocument, EntityType, Handle}; use crate::scene::convert::tess_util::aci_to_rgba; @@ -2148,6 +2618,8 @@ fn tessellate_dimension_inner( real!(dimtfac, DIMTFAC); real!(dimgap, DIMGAP); real!(dimaltrnd, DIMALTRND); + real!(dimaltmzf, DIMALTMZF); + real!(dimmzf, DIMMZF); flag!(dimtol, DIMTOL); flag!(dimlim, DIMLIM); flag!(dimtih, DIMTIH); @@ -2165,6 +2637,7 @@ fn tessellate_dimension_inner( flag!(dimfxlon, DIMFXLON); flag!(dimtxtdirection, DIMTXTDIRECTION); int!(dimzin, DIMZIN); + int!(dimtad, DIMTAD); int!(dimazin, DIMAZIN); int!(dimarcsym, DIMARCSYM); int!(dimclrd, DIMCLRD); @@ -2204,6 +2677,12 @@ fn tessellate_dimension_inner( if let Some(value) = ov::string(data, ov::DIMAPOST) { style.dimapost = value; } + if let Some(value) = ov::string(data, ov::DIMALTMZS) { + style.dimaltmzs = value; + } + if let Some(value) = ov::string(data, ov::DIMMZS) { + style.dimmzs = value; + } if let Some(value) = ov::handle(data, ov::DIMTXSTY) { style.dimtxsty_handle = value; if let Some(record) = document.text_styles.iter().find(|record| record.handle == value) { @@ -2324,12 +2803,15 @@ fn tessellate_dimension_inner( // Text box (local space) so the dim line can be broken where the text // crosses it — lets a DIMTFILL background sit over the line. The renderer // draws 2D fills under all wires, so the line is gapped rather than masked. - let dimgap_local = style.map(|s| (s.dimgap * dim_scale) as f32).unwrap_or(0.09); + let dimgap_local = style + .map(|s| (s.dimgap.abs() * dim_scale) as f32) + .unwrap_or(0.09); + let text_width = dimension_text_value(dim, style) + .map(|text| text.chars().count() as f32 * dim_txt as f32 * 0.6 + dimgap_local * 2.0) + .unwrap_or(0.0); let text_break = { let tp = vec3_local(dimension_text_pos_f64(dim, style, dim_txt, dim_scale)); - let tw = dimension_text_value(dim, style) - .map(|t| t.chars().count() as f32 * dim_txt as f32 * 0.6) - .unwrap_or(0.0); + let tw = (text_width - dimgap_local * 2.0).max(0.0); if tw > 0.0 { // Vertical threshold is the bare text half-height (no DIMGAP): the // line only breaks when it actually passes under the glyphs. Text @@ -2359,6 +2841,9 @@ fn tessellate_dimension_inner( dimcen, ticks: dimtsz_raw > 1e-9, arrow_len: dimasz, + text_width, + dimatfit: style.map(|s| s.dimatfit).unwrap_or(3), + dimtofl: style.map(|s| s.dimtofl).unwrap_or(false), text_break, }, SuppressFlags { @@ -2381,18 +2866,9 @@ fn tessellate_dimension_inner( } } } - // Fit: text that doesn't fit between the extension lines is slid outside - // in the text-placement pass (unless DIMTIX), and arrowheads that don't - // fit are flipped outside in append_linear_dimension. DIMTOFL (force the - // dim line inside) is already satisfied for linear/aligned, whose dim - // line is always drawn between the extension points. DIMATFIT's exact - // mode ordering (arrows-first vs text-first) and DIMUPT (reposition on - // create) aren't differentiated yet — read here for round-trip. - let _ = (s.dimtofl, s.dimatfit, s.dimupt); - // DIMTXTDIRECTION (RTL) needs per-instance text mirroring on the Text - // entity, which the current text struct can't carry. Tracked: read - // and ignore so the file round-trips on save. - let _ = s.dimtxtdirection; + // DIMUPT governs interactive creation-time text placement; saved + // geometry already carries the resulting position. + let _ = s.dimupt; // DIMARCSYM only applies to arc-length dims; DIMJOGANG only to // jogged-radius dims. We don't ship those Dimension variants yet, // so the values are read for round-trip but not drawn. @@ -2672,6 +3148,49 @@ fn tessellate_dimension_inner( } } + // A negative DIMGAP denotes the Basic tolerance display: frame the + // dimension text while keeping the absolute gap as the frame margin. + if style.is_some_and(|s| s.dimgap < 0.0) { + if let Some(rect) = text_fill_rect(dim, style, dim_txt, dim_scale) { + let p1 = rect[0]; + let p2 = rect[1]; + let p3 = rect[2]; + let p4 = rect[5]; + wires.push(WireModel { + point_marker: None, + taper_widths: Vec::new(), + world_width: 0.0, + depth_override: None, + display_visible: true, + plot_visible: true, + fill_is_3d: false, + fill_is_2d_solid: false, + render_instance: None, + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), + dash_from_start: false, + dash_align_end: None, + text_verts: Vec::new(), + name: name.clone(), + points: vec![p1, p2, p2, p3, p3, p4, p4, p1], + points_low: Vec::new(), + color: if selected { WireModel::SELECTED } else { text_color }, + selected, + aci: 0, + pattern_length: 0.0, + pattern: [0.0; 8], + line_weight_px: 1.0, + snap_pts: vec![], + tangent_geoms: vec![], + key_vertices: vec![], + aabb: WireModel::UNBOUNDED_AABB, + plinegen: true, + fill_tris: vec![], + fill_tris_low: Vec::new(), + }); + } + } + if let Some(synth_text_entity) = dimension_text_entity(dim, dim_txt, style, document, dim_scale) { // Tolerance Text rendered separately so DIMTFAC scales its height @@ -2837,7 +3356,7 @@ fn text_fill_rect( return None; } let pos = dimension_text_pos_f64(dim, style, text_height, dim_scale); - let dimgap = style.map(|s| s.dimgap).unwrap_or(0.0).max(0.0) * dim_scale; + let dimgap = style.map(|s| s.dimgap.abs()).unwrap_or(0.0) * dim_scale; // ~0.6 × text_height per character; matches average glyph aspect for // the bundled stick fonts. Inflate by 1 DIMGAP on each side. let approx_w = value.chars().count() as f64 * text_height * 0.6 + dimgap * 2.0; @@ -2883,6 +3402,9 @@ struct DimLineParams { ticks: bool, /// Arrowhead length (DIMASZ, scaled) — used to decide arrow-outside fit. arrow_len: f32, + text_width: f32, + dimatfit: i16, + dimtofl: bool, /// Text box (local centre, half-width, half-height) used to break the /// dimension line where the text sits on it, so a DIMTFILL background reads /// over the line. None when the text doesn't overlap the line. @@ -3190,12 +3712,22 @@ fn append_linear_dimension( let d1_out = d1 - dir_d1_to_d2 * dle; let d2_out = d2 + dir_d1_to_d2 * dle; - // DIMATFIT fit: when the arrowheads don't fit between the extension lines - // they're flipped to the outside (point still on the ext line, body - // outside) with a short stub for them to sit on. DIMSOXD suppresses those - // outer stubs. Ticks always fit, so this only affects arrowheads. + // When text plus arrows do not fit, DIMATFIT decides which component moves + // first: 0=both, 1=arrows, 2=text, 3=best fit. let gap = (d2 - d1).length(); - let arrows_outside = !params.ticks && params.arrow_len > 1e-6 && gap < 2.0 * params.arrow_len; + let arrows_outside = if params.ticks || params.arrow_len <= 1e-6 { + false + } else if gap < 2.0 * params.arrow_len { + true + } else if gap < params.text_width + 2.0 * params.arrow_len { + match params.dimatfit { + 0 | 1 => true, + 2 => false, + _ => params.text_width <= gap, + } + } else { + false + }; // The two suppression flags address the portions at the first and second // measured points independently. The text gap is also the natural split; @@ -3221,14 +3753,15 @@ fn append_linear_dimension( } } } - if !suppress.dim1 && left_end > 1e-6 { + let draw_inside_line = !arrows_outside || params.dimtofl; + if draw_inside_line && !suppress.dim1 && left_end > 1e-6 { add_segment( &mut g.dim_lines, d1_out, d1_out + line_dir * left_end, ); } - if !suppress.dim2 && line_len - right_start > 1e-6 { + if draw_inside_line && !suppress.dim2 && line_len - right_start > 1e-6 { add_segment( &mut g.dim_lines, d1_out + line_dir * right_start, @@ -3539,7 +4072,9 @@ fn dimension_text_entity( document: &CadDocument, dim_scale: f64, ) -> Option { - let value = dimension_text_value(dim, style)?; + // Tolerances are emitted by `dimension_tolerance_entity` at their own + // height and alignment; keep the primary entity free of duplicate text. + let (value, _) = dimension_text_parts(dim, style)?; // Use f64 position directly to avoid f32 round-trip precision loss at large // coordinates (e.g. Turkish UTM ~4,000,000 m). tessellate() will apply // world_offset when rendering this synthetic entity. @@ -3601,6 +4136,11 @@ fn dimension_text_entity( return Some(EntityType::MText(mtext)); } + let value = if style.is_some_and(|style| style.dimtxtdirection) { + value.chars().rev().collect() + } else { + value + }; let mut text = Text::with_value(value, pos_f64) .with_height(text_height) .with_rotation(rotation); @@ -3654,11 +4194,12 @@ fn dimension_text_rotation(dim: &Dimension, style: Option<&DimStyle>) -> f64 { let dimtih = style.map(|s| s.dimtih).unwrap_or(false); let dimtoh = style.map(|s| s.dimtoh).unwrap_or(false); let dimjust = style.map(|s| s.dimjust).unwrap_or(0); + let outside = dimension_text_is_outside(dim, style); if base.text_rotation.abs() > 1e-9 { base.text_rotation } else if base.horizontal_direction.abs() > 1e-9 { base.horizontal_direction - } else if dimtih || dimtoh { + } else if (outside && dimtoh) || (!outside && dimtih) { 0.0 } else { let mut r = dimension_text_natural_rotation(dim); @@ -3669,6 +4210,52 @@ fn dimension_text_rotation(dim: &Dimension, style: Option<&DimStyle>) -> f64 { } } +fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool { + let Some(style) = style else { + return false; + }; + let (first, second, axis) = match dim { + Dimension::Linear(d) => ( + d.first_point, + d.second_point, + Vector3::new(d.rotation.cos(), d.rotation.sin(), 0.0), + ), + Dimension::Aligned(d) => { + let delta = d.second_point - d.first_point; + let length = (delta.x * delta.x + delta.y * delta.y).sqrt().max(1e-12); + (d.first_point, d.second_point, delta / length) + } + _ => return false, + }; + let first_axis = first.x * axis.x + first.y * axis.y; + let second_axis = second.x * axis.x + second.y * axis.y; + let lo = first_axis.min(second_axis); + let hi = first_axis.max(second_axis); + if dim.base().text_user_positioned { + let point = dim.base().text_middle_point; + let position = point.x * axis.x + point.y * axis.y; + return position < lo || position > hi; + } + if style.dimtix { + return false; + } + let scale = if style.dimscale > 1e-9 { style.dimscale } else { 1.0 }; + let height = style.dimtxt * scale; + let gap = style.dimgap.abs() * scale; + let text_width = dimension_text_value(dim, Some(style)) + .map(|value| value.chars().count() as f64 * height * 0.6 + gap * 2.0) + .unwrap_or(0.0); + let arrow = style.dimasz * scale; + let span = hi - lo; + let insufficient = text_width + arrow * 2.0 > span; + insufficient + && match style.dimatfit { + 0 | 2 => true, + 1 | 3 => text_width > span, + _ => text_width > span, + } +} + fn dimension_text_natural_rotation(dim: &Dimension) -> f64 { let angle = match dim { Dimension::Linear(d) => d.rotation, @@ -3726,7 +4313,7 @@ fn dimension_text_parts( // Build tolerance / limits suffix separately so the caller can render // it as its own Text entity at DIMTFAC × DIMTXT height. - let tolerance_suffix = build_tolerance_suffix(dim.measurement(), style, is_angular); + let tolerance_suffix = build_tolerance_suffix(dim, style, is_angular); let primary = apply_dimpost(&primary_raw, style); // Alternate units appended in brackets when DIMALT is on (linear only). @@ -3754,21 +4341,22 @@ fn dimension_text_parts( } fn build_tolerance_suffix( - measurement: f64, + dim: &Dimension, style: Option<&DimStyle>, is_angular: bool, ) -> Option { let s = style?; + let measurement = dim.measurement(); let dimtdec = s.dimtdec.max(0) as usize; let dimtzin = s.dimtzin; let fmt = |v: f64| -> String { let raw = format!("{:.*}", dimtdec, v); - apply_linear_zero_suppression(&raw, dimtzin) + swap_decimal_sep(&apply_linear_zero_suppression(&raw, dimtzin), s.dimdsep) }; if s.dimlim { let high = measurement + s.dimtp; let low = measurement - s.dimtm; - return Some(format!("{}/{}", fmt(high), fmt(low))); + return Some(format!("\\S{}^{};", fmt(high), fmt(low))); } if s.dimtol { let unit = if is_angular { "°" } else { "" }; @@ -3776,18 +4364,44 @@ fn build_tolerance_suffix( return Some(format!("±{}{}", fmt(s.dimtp), unit)); } if s.dimtp.abs() > 1e-12 || s.dimtm.abs() > 1e-12 { + let mut upper = fmt(s.dimtp); + let mut lower = fmt(s.dimtm); + let alignment = crate::entities::dim_override::int( + &dim.base().common.extended_data, + crate::entities::dim_override::DIMTALN, + ) + .unwrap_or(0); + if alignment == 0 { + align_tolerance_decimals(&mut upper, &mut lower, s.dimdsep); + } return Some(format!( - "+{}{} / -{}{}", - fmt(s.dimtp), - unit, - fmt(s.dimtm), - unit + "\\S+{}{}^-{}{};", + upper, unit, lower, unit )); } } None } +fn align_tolerance_decimals(upper: &mut String, lower: &mut String, separator: i16) { + let separator = separator as u8 as char; + let integer_width = |value: &str| { + value + .find(separator) + .or_else(|| value.find('.')) + .unwrap_or(value.len()) + }; + let upper_width = integer_width(upper); + let lower_width = integer_width(lower); + let target = upper_width.max(lower_width); + if upper_width < target { + upper.insert_str(0, &" ".repeat(target - upper_width)); + } + if lower_width < target { + lower.insert_str(0, &" ".repeat(target - lower_width)); + } +} + /// Build the bracketed alternate-units suffix when DIMALT is enabled. /// When DIMTOL is also on, the bracketed text includes the tolerance /// component formatted with DIMALTTD / DIMALTTZ. @@ -3800,10 +4414,19 @@ fn alternate_units_text(measurement: f64, style: Option<&DimStyle>) -> Option 1e-12 { v = (v / s.dimaltrnd).round() * s.dimaltrnd; } + let use_sub_units = s.dimaltz & 4 != 0 + && v.abs() < 1.0 + && s.dimaltmzf.abs() > 1e-12; + if use_sub_units { + v *= s.dimaltmzf; + } let dec = s.dimaltd.max(0) as usize; - let raw = format_with_unit(v, s.dimaltu, dec, s.dimfrac, s.dimaltz); + let raw = format_with_unit(v, s.dimaltu, dec, s.dimfrac, s.dimaltz, true); let suppressed = apply_linear_zero_suppression(&raw, s.dimaltz); - let sep_swapped = swap_decimal_sep(&suppressed, s.dimdsep); + let mut sep_swapped = swap_decimal_sep(&suppressed, s.dimdsep); + if use_sub_units { + sep_swapped.push_str(&s.dimaltmzs); + } // Alt-unit tolerance suffix using DIMALTTD / DIMALTTZ. let alt_value = if s.dimtol { let alttdec = s.dimalttd.max(0) as usize; @@ -3857,7 +4480,7 @@ fn dimension_tolerance_entity( ) -> Option { let s = style?; let is_angular = matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)); - let tol = build_tolerance_suffix(dim.measurement(), style, is_angular)?; + let tol = build_tolerance_suffix(dim, style, is_angular)?; let dimtfac = if s.dimtfac.abs() < 1e-12 { 1.0 } else { @@ -3889,7 +4512,12 @@ fn dimension_tolerance_entity( // Approximate widths from glyph counts (~0.6 × cell size per char). let primary_w = primary_value_len as f64 * primary_height * 0.6; - let tol_w = tol.chars().count() as f64 * tol_height * 0.6; + let tol_visible_chars = tol + .strip_prefix("\\S") + .and_then(|value| value.strip_suffix(';')) + .map(|value| value.split('^').map(str::len).max().unwrap_or(0)) + .unwrap_or_else(|| tol.chars().count()); + let tol_w = tol_visible_chars as f64 * tol_height * 0.6; let gap = primary_height * 0.2; let dx_local = primary_w * 0.5 + tol_w * 0.5 + gap; let dy_local = match s.dimtolj { @@ -3904,6 +4532,16 @@ fn dimension_tolerance_entity( primary_insertion.y + dx_local * sr + dy_local * cr, primary_insertion.z, ); + if value_has_mtext_codes(&tol) { + let mut mtext = MText::with_value(tol, pos); + mtext.height = tol_height; + mtext.rotation = rot; + mtext.style = primary_style; + mtext.attachment_point = acadrust::entities::AttachmentPoint::MiddleCenter; + mtext.common = primary_common; + return Some(EntityType::MText(mtext)); + } + let mut t = Text::with_value(tol, pos) .with_height(tol_height) .with_rotation(rot); @@ -3930,23 +4568,39 @@ fn apply_dimpost(value: &str, style: Option<&DimStyle>) -> String { /// Format a linear measurement honouring DIMLFAC, DIMRND, DIMDEC, DIMZIN, DIMDSEP, DIMLUNIT. fn format_linear_value(measurement: f64, style: Option<&DimStyle>) -> String { - let (dec, zin, lfac, rnd, dsep, lunit, frac) = style + let (dec, zin, lfac, rnd, dsep, lunit, frac, sub_factor, sub_suffix) = style .map(|s| { ( - s.dimdec, s.dimzin, s.dimlfac, s.dimrnd, s.dimdsep, s.dimlunit, s.dimfrac, + s.dimdec, + s.dimzin, + s.dimlfac, + s.dimrnd, + s.dimdsep, + s.dimlunit, + s.dimfrac, + s.dimmzf, + s.dimmzs.as_str(), ) }) - .unwrap_or((4, 8, 1.0, 0.0, 46, 2, 0)); + .unwrap_or((4, 8, 1.0, 0.0, 46, 2, 0, 1.0, "")); let lfac = if lfac.abs() < 1e-12 { 1.0 } else { lfac }; let mut v = measurement * lfac; if rnd > 1e-12 { v = (v / rnd).round() * rnd; } + let use_sub_units = zin & 4 != 0 && v.abs() < 1.0 && sub_factor.abs() > 1e-12; + if use_sub_units { + v *= sub_factor; + } let dec = dec.max(0) as usize; - let raw = format_with_unit(v, lunit, dec, frac, zin); + let raw = format_with_unit(v, lunit, dec, frac, zin, false); let suppressed = apply_linear_zero_suppression(&raw, zin); - swap_decimal_sep(&suppressed, dsep) + let mut formatted = swap_decimal_sep(&suppressed, dsep); + if use_sub_units { + formatted.push_str(sub_suffix); + } + formatted } /// Dispatch on DIMLUNIT / DIMALTU. @@ -3955,15 +4609,23 @@ fn format_linear_value(measurement: f64, style: Option<&DimStyle>) -> String { /// 3 = Engineering (feet + decimal inches; 1 unit = 1 inch) /// 4 = Architectural (feet + fractional inches) /// 5 = Fractional (integer + fractional inches) -/// 6 = Windows desktop → falls back to Decimal -/// `dimfrac` controls denominator power for arch/fractional output (0/1/2); -/// rendered inline as "n/d" (stacked glyphs require MText support). -fn format_with_unit(value: f64, unit: i16, dec: usize, dimfrac: i16, zin: i16) -> String { +/// For alternate units, 4/5 are stacked and 6/7 are unstacked architectural +/// and fractional forms. For primary units DIMFRAC selects the stack form. +fn format_with_unit( + value: f64, + unit: i16, + dec: usize, + dimfrac: i16, + zin: i16, + alternate: bool, +) -> String { match unit { 1 => format!("{:.*e}", dec, value), 3 => format_engineering(value, dec), - 4 => format_architectural(value, dimfrac, zin), - 5 => format_fractional(value, dimfrac), + 4 => format_architectural(value, dec, if alternate { 0 } else { dimfrac }, zin), + 5 => format_fractional(value, dec, if alternate { 0 } else { dimfrac }), + 6 if alternate => format_architectural(value, dec, 2, zin), + 7 if alternate => format_fractional(value, dec, 2), _ => format!("{:.*}", dec, value), } } @@ -3976,8 +4638,8 @@ fn format_engineering(inches: f64, dec: usize) -> String { format!("{}{:.0}'-{:.*}\"", sign, feet, dec, rem_in) } -fn format_architectural(inches: f64, dimfrac: i16, zin: i16) -> String { - let denom = arch_denom(dimfrac); +fn format_architectural(inches: f64, precision: usize, dimfrac: i16, zin: i16) -> String { + let denom = fractional_denominator(precision); // Round to the nearest 1/denom inch *first*, in integer ticks, so a fraction // that rounds up to a whole inch carries into the inches — and on into the // feet — instead of printing an un-reduced "11 1/1". A 3ft object that @@ -3988,7 +4650,7 @@ fn format_architectural(inches: f64, dimfrac: i16, zin: i16) -> String { let feet = ticks / per_foot; let rem = ticks % per_foot; let whole = rem / denom; - let frac_str = reduce_fraction(rem % denom, denom); + let frac_str = format_fraction_component(&reduce_fraction(rem % denom, denom), dimfrac); // DIMZIN feet/inch suppression: // 0 suppress zero feet & zero inches, 1 include both, @@ -4024,13 +4686,13 @@ fn format_architectural(inches: f64, dimfrac: i16, zin: i16) -> String { format!("{}{}", sign, body) } -fn format_fractional(value: f64, dimfrac: i16) -> String { - let denom = arch_denom(dimfrac); +fn format_fractional(value: f64, precision: usize, dimfrac: i16) -> String { + let denom = fractional_denominator(precision); // Round on the fraction grid first (same carry reasoning as architectural). let ticks = (value.abs() * denom as f64).round() as u64; let sign = if value < 0.0 && ticks != 0 { "-" } else { "" }; let whole = ticks / denom; - let frac_str = reduce_fraction(ticks % denom, denom); + let frac_str = format_fraction_component(&reduce_fraction(ticks % denom, denom), dimfrac); if frac_str.is_empty() { format!("{}{}", sign, whole) } else if whole == 0 { @@ -4040,12 +4702,18 @@ fn format_fractional(value: f64, dimfrac: i16) -> String { } } -/// Power-of-two denominator for architectural / fractional output. DIMFRAC is -/// the exponent-like fraction-format knob; clamp it to a readable -/// range (16ths … 1024ths). -fn arch_denom(dimfrac: i16) -> u64 { - let exp = (dimfrac.clamp(0, 8) as u32).max(2) + 2; // 4..=10 → 16..=1024 - 1u64 << exp +/// Fraction precision maps 0..8 to whole units through 1/256. DIMFRAC controls +/// only the visual stack form; it must not change the numeric denominator. +fn fractional_denominator(precision: usize) -> u64 { + 1u64 << precision.min(8) +} + +fn format_fraction_component(value: &str, dimfrac: i16) -> String { + if value.is_empty() || dimfrac == 2 { + return value.to_string(); + } + let separator = if dimfrac == 1 { '#' } else { '/' }; + format!("\\S{};", value.replacen('/', &separator.to_string(), 1)) } /// Reduce a power-of-two fraction numer/denom to display form. Empty for a zero @@ -4217,6 +4885,7 @@ fn text_on_dim_line( text_w: f64, arrow: f64, dimtix: bool, + dimatfit: i16, tad: i16, ) -> Vector3 { // The perpendicular "up" side: the perpendicular of the dimension line @@ -4256,13 +4925,20 @@ fn text_on_dim_line( 2 | 4 => t2, _ => (t1 + t2) * 0.5, }; - // DIMATFIT / DIMTIX fit: when centred text can't fit between the extension - // lines, slide it just past the far one (text-outside placement) unless - // DIMTIX forces it to stay inside. + // DIMATFIT / DIMTIX fit: move text according to the selected priority when + // the combined text-and-arrow envelope cannot fit. if dimjust == 0 && !dimtix && text_w > 0.0 { let lo = t1.min(t2); let hi = t1.max(t2); - if text_w > (hi - lo) - 2.0 * arrow { + let span = hi - lo; + let insufficient = text_w + 2.0 * arrow > span; + let text_outside = insufficient + && match dimatfit { + 0 | 2 => true, + 1 | 3 => text_w > span, + _ => text_w > span, + }; + if text_outside { along = hi + arrow + text_w * 0.5; } } @@ -4289,10 +4965,11 @@ fn dimension_text_pos_f64( let dimtad = style.map(|s| s.dimtad).unwrap_or(1); // DIMGAP scales with DIMSCALE just like DIMTXT, so the text-to-line gap // stays consistent when DIMSCALE != 1. - let dimgap = style.map(|s| s.dimgap).unwrap_or(0.0) * dim_scale; + let dimgap = style.map(|s| s.dimgap.abs()).unwrap_or(0.0) * dim_scale; let dimjust = style.map(|s| s.dimjust).unwrap_or(0); // DIMTIX forces the text to stay between the extension lines. let dimtix = style.map(|s| s.dimtix).unwrap_or(false); + let dimatfit = style.map(|s| s.dimatfit).unwrap_or(3); // DIMTVP vertical-position multiplier (units of dimtxt). Only honoured when // DIMTAD == 0; offsets text perpendicular to the dim line. let dimtvp = style.map(|s| s.dimtvp).unwrap_or(0.0); @@ -4332,6 +5009,7 @@ fn dimension_text_pos_f64( text_w, arrow, dimtix, + dimatfit, dimtad, ) } @@ -4350,6 +5028,7 @@ fn dimension_text_pos_f64( text_w, arrow, dimtix, + dimatfit, dimtad, ) } @@ -4465,11 +5144,11 @@ mod dimtad_tests { fn above_is_up_regardless_of_direction() { let (first, second, defpt) = (v(0.0, 10.0), v(20.0, 10.0), v(0.0, 0.0)); let perp = 5.0; - let fwd = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 1); - let rev = text_on_dim_line(first, second, defpt, -1.0, 0.0, 0, perp, 0.0, 1.0, false, 1); + let fwd = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 1); + let rev = text_on_dim_line(first, second, defpt, -1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 1); assert!(fwd.y > 0.0, "above must be +Y, got {}", fwd.y); assert!(rev.y > 0.0, "above must be +Y even reversed, got {}", rev.y); - let below = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 4); + let below = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 4); assert!(below.y < 0.0, "below must be -Y, got {}", below.y); } @@ -4490,6 +5169,7 @@ mod dimtad_tests { 0.0, 1.0, false, + 3, 2, ); assert!( @@ -4509,6 +5189,7 @@ mod dimtad_tests { 0.0, 1.0, false, + 3, 2, ); assert!( @@ -4528,30 +5209,30 @@ mod arch_format_tests { // for the fraction that reduced to 1/1 and leaked out as a bare "1".) #[test] fn arch_carries_fraction_up_to_feet() { - assert_eq!(format_architectural(35.99, 0, 1), "3'-0\""); - assert_eq!(format_architectural(36.0, 0, 1), "3'-0\""); + assert_eq!(format_architectural(35.99, 4, 2, 1), "3'-0\""); + assert_eq!(format_architectural(36.0, 4, 2, 1), "3'-0\""); // An exact 15/16 must still render as a fraction, no spurious carry. - assert_eq!(format_architectural(35.9375, 0, 1), "2'-11 15/16\""); + assert_eq!(format_architectural(35.9375, 4, 2, 1), "2'-11 15/16\""); } // Carry that stops at inches (11.999" → 12" → 1'-0", not "0'-11 1""). #[test] fn arch_carries_fraction_up_to_inches() { - assert_eq!(format_architectural(11.999, 0, 1), "1'-0\""); + assert_eq!(format_architectural(11.999, 4, 2, 1), "1'-0\""); } #[test] fn arch_normal_values_unchanged() { - assert_eq!(format_architectural(30.5, 0, 1), "2'-6 1/2\""); - assert_eq!(format_architectural(0.0, 0, 1), "0'-0\""); - assert_eq!(format_architectural(-30.25, 0, 1), "-2'-6 1/4\""); + assert_eq!(format_architectural(30.5, 4, 2, 1), "2'-6 1/2\""); + assert_eq!(format_architectural(0.0, 4, 2, 1), "0'-0\""); + assert_eq!(format_architectural(-30.25, 4, 2, 1), "-2'-6 1/4\""); } // Same carry bug lived in the plain fractional formatter. #[test] fn fractional_carries_up() { - assert_eq!(format_fractional(35.9999, 0), "36"); - assert_eq!(format_fractional(11.999, 0), "12"); - assert_eq!(format_fractional(6.5, 0), "6 1/2"); + assert_eq!(format_fractional(35.9999, 4, 2), "36"); + assert_eq!(format_fractional(11.999, 4, 2), "12"); + assert_eq!(format_fractional(6.5, 4, 2), "6 1/2"); } }