diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 97a03e26f..77a1e1756 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -801,7 +801,21 @@ impl OpenCADStudio { // the structure snapshot fallback. let delta_safe = self.delta_add_safe(i, &entity); let pending = self.begin_undo(i, label, 1, delta_safe); - self.commit_entity(entity); + let is_linear_dimension = matches!( + entity, + acadrust::EntityType::Dimension(acadrust::entities::Dimension::Linear(_)) + ); + let committed = self.commit_entity_handle(entity); + if is_linear_dimension { + if let Some(handle) = committed { + let sources = self.tabs[i] + .scene + .infer_linear_dimension_sources(handle); + self.tabs[i] + .scene + .attach_linear_dimension_association(handle, sources); + } + } self.tabs[i].dirty = true; let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { @@ -1191,7 +1205,21 @@ impl OpenCADStudio { let label = self.history_label_from_active_cmd(i, "ENTITY"); let delta_safe = self.delta_add_safe(i, &entity); let pending = self.begin_undo(i, label, 1, delta_safe); - self.commit_entity(entity); + let is_linear_dimension = matches!( + entity, + acadrust::EntityType::Dimension(acadrust::entities::Dimension::Linear(_)) + ); + let committed = self.commit_entity_handle(entity); + if is_linear_dimension { + if let Some(handle) = committed { + let sources = self.tabs[i] + .scene + .infer_linear_dimension_sources(handle); + self.tabs[i] + .scene + .attach_linear_dimension_association(handle, sources); + } + } self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; @@ -1201,6 +1229,27 @@ impl OpenCADStudio { self.commit_undo_delta(i, pd); } } + CmdResult::CommitAssociativeDimension { entity, source } => { + let label = self.history_label_from_active_cmd(i, "DIMLINEAR"); + let pending = self.begin_undo(i, label, 1, false); + if let Some(handle) = self.commit_entity_handle(entity) { + self.tabs[i] + .scene + .attach_linear_dimension_association(handle, [Some(source), Some(source)]); + self.tabs[i].scene.bump_entities(&[ + (handle, crate::scene::ChangeKind::Modified), + (source, crate::scene::ChangeKind::Modified), + ]); + } + self.tabs[i].dirty = true; + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.restore_pre_cmd_tangent(); + if let Some(pending) = pending { + self.commit_undo_delta(i, pending); + } + } CmdResult::CommitSolid { entity, solid, diff --git a/src/app/mod.rs b/src/app/mod.rs index 9e5877083..3830c21dc 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1218,6 +1218,8 @@ pub enum ColorPickTarget { Properties, /// Selected entities' background colour (hatch / MTEXT background row). PropertiesBg, + /// A named per-entity Properties colour field. + PropertiesField(String), /// Current creation colour (ribbon). Ribbon, /// A layer's colour, by panel row index. diff --git a/src/app/properties.rs b/src/app/properties.rs index a7256c286..9833701d0 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1006,6 +1006,21 @@ impl OpenCADStudio { // Inject DimStyle picker + style-derived groups for Dimensions. if let acadrust::EntityType::Dimension(d) = entity { + if let Some(general) = sections.first_mut() { + general.props.retain(|property| property.field != "handle"); + let associative = + crate::scene::dimension_assoc::dimension_is_associative( + &self.tabs[i].scene.document, + d.base().common.handle, + ); + general.props.push(crate::scene::model::object::Property { + label: t!("Associative").into_owned(), + field: "associative", + value: crate::scene::model::object::PropValue::ReadOnly( + if associative { "Yes" } else { "No" }.to_string(), + ), + }); + } let dim_style_names: Vec = self.tabs[i] .scene .document @@ -1046,12 +1061,14 @@ impl OpenCADStudio { && s.name.eq_ignore_ascii_case("Standard")) }) { - sections.extend(crate::entities::dimension::style_sections(style)); + sections.extend(crate::entities::dimension::style_sections( + style, + d, + &self.tabs[i].scene.document, + )); - // The style groups are a read-only mirror, but the - // dim-line colour is an editable per-object override: - // prefer the ACAD_DSTYLE code-176 (ACI) override, else - // the style's DIMCLRD. + // Prefer entity-level dimension-variable overrides; + // fall back to the assigned style values. use crate::entities::dim_override as dov; use crate::scene::model::object::PropValue; let dim_c = dov::color(&d.base().common.extended_data, dov::DIMCLRD) @@ -1061,6 +1078,20 @@ impl OpenCADStudio { "dim_line_color", PropValue::ColorChoice(dim_c), ); + for (field, code, inherited) in [ + ("dim_ext_line_color", dov::DIMCLRE, style.dimclre), + ("dim_text_color", dov::DIMCLRT, style.dimclrt), + ] { + let color = dov::color(&d.base().common.extended_data, code) + .unwrap_or_else(|| { + acadrust::types::Color::from_index(inherited) + }); + set_row_value( + &mut sections, + field, + PropValue::ColorChoice(color), + ); + } } } @@ -1323,10 +1354,8 @@ impl OpenCADStudio { } // Annotative Yes/No + a conditional "Annotative scale" row. - // Read-only: annotative state comes from the entity's style - // (or its own flag) and the assigned annotation-scale name(s) - // are walked from the entity's extension dictionary — both - // need the document, so they are resolved here. + // Annotative state and assigned scale names need the + // document, so they are resolved here. { // Which entities show an Annotative row, the field it uses, // and — for those that don't already carry the row @@ -1368,7 +1397,7 @@ impl OpenCADStudio { // (MTEXT via its native flag, single-line TEXT via the // context alone) get an editable toggle: turning it on // synthesizes a real per-scale representation. The - // remaining types are style-driven and stay read-only. + // remaining style-only types stay read-only. if anno_field == "annotative" { match entity { acadrust::EntityType::MText(t) => set_row_value( @@ -1381,7 +1410,8 @@ impl OpenCADStudio { ), acadrust::EntityType::Text(_) | acadrust::EntityType::Insert(_) - | acadrust::EntityType::Hatch(_) => set_row_value( + | acadrust::EntityType::Hatch(_) + | acadrust::EntityType::Dimension(_) => set_row_value( &mut sections, "annotative", crate::scene::model::object::PropValue::BoolToggle { @@ -1397,16 +1427,26 @@ impl OpenCADStudio { } } if is_anno { - // The applied annotation scale follows the current - // annotation scale (CANNOSCALE / the status-bar - // scale pill), not a per-object stored value. + let memberships = crate::scene::annotative::object_scale_memberships( + doc, + entity.common().handle, + ); + let assigned_scales = if memberships.is_empty() { + doc.header.current_annotation_scale.clone() + } else { + memberships + .into_iter() + .map(|(name, _)| name) + .collect::>() + .join(", ") + }; insert_row_after( &mut sections, anno_field, crate::entities::common::ro_prop( t!("Annotative scale").as_ref(), "annotative_scale", - doc.header.current_annotation_scale.clone(), + assigned_scales, ), ); } @@ -2411,18 +2451,7 @@ fn set_row_value( /// The lineweight dropdown options (named defaults + the standard millimetre /// steps), matching the labels `dim_lineweight_label` produces. pub(crate) fn lineweight_options() -> Vec { - let mut v = vec![ - "ByLayer".to_string(), - "ByBlock".to_string(), - "Default".to_string(), - ]; - for lw in [ - 0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, - 200, 211, - ] { - v.push(format!("{:.2} mm", lw as f64 / 100.0)); - } - v + crate::entities::common::lineweight_options() } /// Inverse of `dim_lineweight_label`: a lineweight label → DIMLWD enum value. @@ -2548,13 +2577,7 @@ fn leader_arrow_label( /// DIMLWD lineweight enum → label. fn dim_lineweight_label(dimlwd: i16) -> String { - match dimlwd { - -1 => "ByLayer".to_string(), - -2 => "ByBlock".to_string(), - -3 => "Default".to_string(), - v if v >= 0 => format!("{:.2} mm", v as f64 / 100.0), - _ => "Default".to_string(), - } + crate::entities::common::lineweight_label(dimlwd) } /// Human-readable INSUNITS name (DXF group 70 unit codes). diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 83d007583..4ec4c44f6 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1140,6 +1140,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { if matches!( item.action, GripMenuAction::Stretch + | GripMenuAction::MoveWithDimLine | GripMenuAction::MoveWithLeader | GripMenuAction::MoveIndependent ) { @@ -1839,7 +1840,24 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } self.push_undo_snapshot(i, "CHPROP"); - if field == "vscale_std" { + if field.starts_with("dim_") { + for &handle in &handles { + if self.tabs[i].scene.is_layer_locked(handle) { + continue; + } + if matches!( + self.tabs[i].scene.document.get_entity(handle), + Some(acadrust::EntityType::Dimension(_)) + ) { + crate::entities::dim_override::set_property( + &mut self.tabs[i].scene.document, + handle, + field, + &value, + ); + } + } + } else if field == "vscale_std" { for &handle in &handles { if matches!( self.tabs[i].scene.document.get_entity(handle), @@ -2410,6 +2428,19 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { continue; } match field { + _ if field.starts_with("dim_") => { + if matches!( + self.tabs[i].scene.document.get_entity(handle), + Some(acadrust::EntityType::Dimension(_)) + ) { + crate::entities::dim_override::set_property( + &mut self.tabs[i].scene.document, + handle, + field, + &val, + ); + } + } "hyperlink" => { // Stored in the standard PE_URL XDATA // record; an empty value clears it. diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 679b3ad9a..24bb99c55 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -4834,8 +4834,25 @@ impl OpenCADStudio { // line with the rest of the dim-colour stack (index-only through // the file layer). Guarded to leaders / dimensions so a mixed // selection can't stamp the override onto other entities. - if field == "dim_line_color" { + if matches!( + field.as_str(), + "dim_line_color" + | "dim_ext_line_color" + | "dim_text_color" + | "dim_text_fill_color" + ) { + let fill_mode = (field == "dim_text_fill_color").then(|| match color { + acadrust::types::Color::None => 0, + acadrust::types::Color::ByBlock => 1, + _ => 2, + }); let aci = color.approximate_index(); + let code = match field.as_str() { + "dim_ext_line_color" => crate::entities::dim_override::DIMCLRE, + "dim_text_color" => crate::entities::dim_override::DIMCLRT, + "dim_text_fill_color" => crate::entities::dim_override::DIMTFILLCLR, + _ => crate::entities::dim_override::DIMCLRD, + }; let targets: Vec = handles .iter() .copied() @@ -4850,10 +4867,23 @@ impl OpenCADStudio { if !targets.is_empty() { self.push_undo_snapshot(i, "CHPROP"); for &handle in &targets { + if field == "dim_text_fill_color" { + crate::entities::dim_override::set( + &mut self.tabs[i].scene.document, + handle, + crate::entities::dim_override::DIMTFILL, + Some(acadrust::xdata::XDataValue::Integer16( + fill_mode.unwrap_or(2), + )), + ); + if fill_mode != Some(2) { + continue; + } + } crate::entities::dim_override::set( &mut self.tabs[i].scene.document, handle, - crate::entities::dim_override::DIMCLRD, + code, Some(acadrust::xdata::XDataValue::Integer16(aci)), ); } @@ -7527,6 +7557,7 @@ impl OpenCADStudio { let i = self.active_tab; self.tabs[i].properties.color_picker_open = false; + self.tabs[i].properties.open_color_field = None; self.tabs[i].layers.color_picker_row = None; Task::none() diff --git a/src/app/update/style.rs b/src/app/update/style.rs index 4a13eb631..3574338df 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -1146,6 +1146,9 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { Some(crate::app::ColorPickTarget::PropertiesBg) => { Some(Message::PropBgColorChanged(color)) } + Some(crate::app::ColorPickTarget::PropertiesField(field)) => { + Some(Message::PropColorFieldChanged { field, color }) + } Some(crate::app::ColorPickTarget::MText) => { Some(Message::MTextColorChanged(color)) } diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index 82e6f68b9..b5782f819 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -474,6 +474,7 @@ fn mtext_editor_content<'a>( crate::ui::color_select::ColorExtras { by_layer: true, by_block: false, + ..Default::default() }, Message::MTextColorChanged, Message::MTextColorPickerToggle, diff --git a/src/command.rs b/src/command.rs index 0ed4a77f8..89b8d0382 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1212,6 +1212,11 @@ pub enum CmdResult { CommitEntitiesAndExit(Vec), /// Commit an acadrust entity to the document and end the command. CommitAndExit(EntityType), + /// Commit an object-selected linear dimension and retain its source link. + CommitAssociativeDimension { + entity: EntityType, + source: Handle, + }, /// Commit a Model-tab 3D solid: the acadrust entity (for selection / /// persistence) plus its B-rep (cached for boolean ops + shaded /// rendering). Ends the command. diff --git a/src/entities/common.rs b/src/entities/common.rs index 5e4f98efc..4c8c98f6c 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -575,6 +575,33 @@ pub fn edit_scalar_prop(label: &str, field: &'static str, value: f64) -> Propert } } +/// Standard lineweight choices shared by entity-specific Properties rows. +pub fn lineweight_options() -> Vec { + let mut options = vec![ + "ByLayer".to_string(), + "ByBlock".to_string(), + "Default".to_string(), + ]; + for value in [ + 0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, + 140, 158, 200, 211, + ] { + options.push(format!("{:.2} mm", value as f64 / 100.0)); + } + options +} + +/// DIMLWD/DIMLWE lineweight value displayed by a Properties dropdown. +pub fn lineweight_label(value: i16) -> String { + match value { + -1 => "ByLayer".to_string(), + -2 => "ByBlock".to_string(), + -3 => "Default".to_string(), + value if value >= 0 => format!("{:.2} mm", value as f64 / 100.0), + _ => "Default".to_string(), + } +} + pub fn ro_prop(label: &str, field: &'static str, value: impl Into) -> Property { Property { label: label.into(), diff --git a/src/entities/dim_override.rs b/src/entities/dim_override.rs index 7535f1438..13ce51b8a 100644 --- a/src/entities/dim_override.rs +++ b/src/entities/dim_override.rs @@ -9,7 +9,7 @@ use acadrust::types::Color; use acadrust::xdata::{ExtendedData, XDataValue}; -use acadrust::{CadDocument, Handle}; +use acadrust::{CadDocument, EntityType, Handle}; // DXF group codes of the dimension variables surfaced on the leader panel. pub const DIMSCALE: i16 = 40; // overall scale (real) @@ -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; @@ -229,3 +234,479 @@ pub fn set(doc: &mut CadDocument, handle: Handle, code: i16, value: Option Option { + Some(match field { + "dim_arrow_size" => DIMASZ, + "dim_line_ext" => DIMDLE, + "dim_ext_line_ext" => DIMEXE, + "dim_ext_line_offset" => DIMEXO, + "dim_ext_line_fixed_length" => DIMFXL, + "dim_text_height" => DIMTXT, + "dim_text_offset" => DIMGAP, + "dim_scale_overall" => DIMSCALE, + "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, + _ => return None, + }) +} + +pub fn property_int_code(field: &str) -> Option { + Some(match field { + "dim_line_lineweight" => DIMLWD, + "dim_ext_line_lineweight" => DIMLWE, + "dim_ext_line_fixed" => DIMFXLON, + "dim_text_pos_vert" => DIMTAD, + "dim_text_pos_hor" => DIMJUST, + "dim_text_outside_align" => DIMTOH, + "dim_text_inside_align" => DIMTIH, + "dim_fit" => DIMATFIT, + "dim_text_inside" => DIMTIX, + "dim_text_movement" => DIMTMOVE, + "dim_line_forced" => DIMTOFL, + "dim_line_inside" => DIMSOXD, + "dim_units" => DIMLUNIT, + "dim_precision" => DIMDEC, + "dim_decimal_separator" => DIMDSEP, + "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_sub_units_suffix" => DIMMZS, + "dim_alt_sub_units_suffix" => DIMALTMZS, + _ => return None, + }) +} + +fn yes(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "yes" | "on" | "true" | "1" => Some(1), + "no" | "off" | "false" | "0" => Some(0), + _ => None, + } +} + +fn inherited_int(doc: &CadDocument, handle: Handle, code: i16) -> i16 { + let Some(EntityType::Dimension(dimension)) = doc.get_entity(handle) else { + return 0; + }; + if let Some(value) = int(&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; + }; + match code { + DIMSD1 => style.dimsd1 as i16, + DIMSD2 => style.dimsd2 as i16, + DIMSE1 => style.dimse1 as i16, + DIMSE2 => style.dimse2 as 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, + field: &str, + value: &str, +) -> bool { + let trimmed = value.trim(); + let handle_field = match field { + "dim_arrowhead_1" => Some(DIMBLK1), + "dim_arrowhead_2" => Some(DIMBLK2), + "dim_linetype" => Some(DIMLTYPE), + "dim_ext_linetype_1" => Some(DIMLTEX1), + "dim_ext_linetype_2" => Some(DIMLTEX2), + "dim_text_style" => Some(DIMTXSTY), + _ => None, + }; + if let Some(code) = handle_field { + let resolved = match field { + "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) + } + } + "dim_text_style" => doc + .text_styles + .iter() + .find(|style| style.name == trimmed) + .map(|style| style.handle), + _ => doc + .line_types + .iter() + .find(|line_type| line_type.name == trimmed) + .map(|line_type| line_type.handle), + }; + let Some(resolved) = resolved else { + return false; + }; + set(doc, handle, code, Some(XDataValue::Handle(resolved))); + return true; + } + let inverse_boolean_code = match field { + "dim_line_1" => Some(DIMSD1), + "dim_line_2" => Some(DIMSD2), + "dim_ext_line_1" => Some(DIMSE1), + "dim_ext_line_2" => Some(DIMSE2), + _ => None, + }; + if let Some(code) = inverse_boolean_code { + let Some(visible) = yes(trimmed) else { + return false; + }; + set(doc, handle, code, Some(XDataValue::Integer16((visible == 0) as i16))); + return true; + } + let bit_field = match field { + "dim_suppress_leading_zeros" => Some((DIMZIN, 4)), + "dim_suppress_trailing_zeros" => Some((DIMZIN, 8)), + "dim_alt_suppress_leading_zeros" => Some((DIMALTZ, 4)), + "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 { + let Some(enabled) = yes(trimmed) else { + return false; + }; + let current = inherited_int(doc, handle, code); + let value = if enabled != 0 { + current | bit + } else { + current & !bit + }; + 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 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; + } + return true; + } + if let Some(code) = property_string_code(field) { + set( + doc, + handle, + code, + (!trimmed.is_empty()).then(|| XDataValue::String(value.to_string())), + ); + return true; + } + 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" + | "dim_text_inside" + | "dim_line_forced" + | "dim_line_inside" + | "dim_alt_enabled" => yes(trimmed), + "dim_decimal_separator" => trimmed + .chars() + .next() + .map(|character| character as i16), + "dim_units" => match trimmed.to_ascii_lowercase().as_str() { + "scientific" => Some(1), + "decimal" => Some(2), + "engineering" => Some(3), + "architectural" => Some(4), + "fractional" => Some(5), + "desktop" => Some(6), + _ => trimmed.parse().ok(), + }, + "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() { + "centered" => Some(0), + "above" => Some(1), + "outside" => Some(2), + "jis" => Some(3), + "below" => Some(4), + _ => trimmed.parse().ok(), + }, + "dim_text_pos_hor" => match trimmed.to_ascii_lowercase().as_str() { + "centered" => Some(0), + "at extension line 1" => Some(1), + "at extension line 2" => Some(2), + "over extension line 1" => Some(3), + "over extension line 2" => Some(4), + _ => trimmed.parse().ok(), + }, + "dim_fit" => match trimmed.to_ascii_lowercase().as_str() { + "both text and arrows" => Some(0), + "arrows" => Some(1), + "text" => Some(2), + "best fit" => Some(3), + _ => trimmed.parse().ok(), + }, + "dim_text_movement" => match trimmed.to_ascii_lowercase().as_str() { + "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() { + "bottom" => Some(0), + "middle" => Some(1), + "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 { + return false; + }; + set(doc, handle, code, Some(XDataValue::Integer16(number))); + return true; + } + 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 c3ff26cf7..ad6cd235a 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -8,12 +8,38 @@ use glam::{DVec3, Vec3}; use crate::command::EntityTransform; use crate::entities::common::{ - center_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, + center_grip, edit_angle_prop as edit_angle, edit_prop as edit, lineweight_label, + 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; +pub(crate) fn dimension_text_override(base: &DimensionBase) -> Option<&str> { + base.user_text + .as_deref() + .or_else(|| (!base.text.is_empty()).then_some(base.text.as_str())) +} + +pub(crate) fn set_dimension_text_override(base: &mut DimensionBase, text: Option) { + base.text = text.clone().unwrap_or_default(); + base.user_text = text; +} + +fn dimension_definition_point(dim: &Dimension) -> acadrust::types::Vector3 { + match dim { + Dimension::Aligned(d) => d.definition_point, + Dimension::Linear(d) => d.definition_point, + Dimension::Radius(d) => d.definition_point, + Dimension::Diameter(d) => d.definition_point, + Dimension::Angular2Ln(d) => d.definition_point, + Dimension::Angular3Pt(d) => d.definition_point, + Dimension::Ordinate(d) => d.definition_point, + Dimension::Arc(d) => d.definition_point, + Dimension::LargeRadial(d) => d.definition_point, + } +} + fn base_props(base: &DimensionBase) -> Vec { vec![ crate::scene::model::object::Property { @@ -60,6 +86,27 @@ fn base_props(base: &DimensionBase) -> Vec Vec { + if let Dimension::Linear(d) = dim { + let base = &d.base; + return vec![PropSection { + title: t!("Misc").into_owned(), + props: vec![ + crate::scene::model::object::Property { + label: t!("Dimension style").into_owned(), + field: "style_name", + value: crate::scene::model::object::PropValue::PlainText( + base.style_name.clone(), + ), + }, + edit_angle(t!("Dim line angle").as_ref(), "rotation", d.rotation.to_degrees()), + edit_angle( + t!("Extension line angle").as_ref(), + "ext_line_rotation", + d.ext_line_rotation.to_degrees(), + ), + ], + }]; + } let mut props = base_props(dim.base()); match dim { Dimension::Aligned(d) => { @@ -234,16 +281,12 @@ fn angular_props( fn apply_base_prop(base: &mut DimensionBase, field: &str, value: &str) -> bool { match field { - "text" => { - base.text = value.to_string(); - true - } - "user_text" => { - base.user_text = if value.trim().is_empty() { + "text" | "user_text" | "text_override" => { + set_dimension_text_override(base, if value.trim().is_empty() { None } else { Some(value.to_string()) - }; + }); true } "style_name" => { @@ -305,6 +348,8 @@ fn apply_geom_prop(dim: &mut Dimension, field: &str, value: &str) { Dimension::Arc(d) => apply_arc_fields(d, field, value), Dimension::LargeRadial(d) => apply_large_radial_fields(d, field, value), } + let definition_point = dimension_definition_point(dim); + dim.base_mut().definition_point = definition_point; dim.base_mut().actual_measurement = dim.measurement(); } @@ -1031,6 +1076,24 @@ fn dimension_line_grip_position(dim: &Dimension) -> Option { Some((p1 + p2) * 0.5) } +fn above_dimension_text_position(dim: &Dimension) -> Option { + let center = dimension_line_grip_position(dim)?; + let (ax, ay) = match dim { + Dimension::Linear(d) => (d.rotation.cos(), d.rotation.sin()), + Dimension::Aligned(d) => { + let dx = d.second_point.x - d.first_point.x; + let dy = d.second_point.y - d.first_point.y; + let length = (dx * dx + dy * dy).sqrt(); + if length <= 1e-12 { + return None; + } + (dx / length, dy / length) + } + _ => return Some(center + DVec3::Y), + }; + Some(center + DVec3::new(-ay, ax, 0.0)) +} + impl Grippable for Dimension { fn grips(&self) -> Vec { // Auto-placed dimensions carry a zero text_middle_point sentinel; put @@ -1212,6 +1275,8 @@ impl Grippable for Dimension { restore_dim_text_relative_position(self, saved); } + let definition_point = dimension_definition_point(self); + self.base_mut().definition_point = definition_point; self.base_mut().actual_measurement = self.measurement(); } @@ -1301,33 +1366,62 @@ impl Grippable for Dimension { b.text_middle_point.x = 0.0; b.text_middle_point.y = 0.0; b.text_middle_point.z = 0.0; + b.text_user_positioned = false; } A::Center if grip_id == text_grip => { - // Snap text to the centre of the dimension line. - // Approximate as midpoint of first/second extension - // origins for Linear / Aligned dimensions. - match self { - Dimension::Linear(d) => { - let mx = (d.first_point.x + d.second_point.x) * 0.5; - let my = (d.first_point.y + d.second_point.y) * 0.5; - d.base.text_middle_point.x = mx; - d.base.text_middle_point.y = my; - } - Dimension::Aligned(d) => { - let mx = (d.first_point.x + d.second_point.x) * 0.5; - let my = (d.first_point.y + d.second_point.y) * 0.5; - d.base.text_middle_point.x = mx; - d.base.text_middle_point.y = my; - } - _ => {} + if let Some(point) = dimension_line_grip_position(self) { + let base = self.base_mut(); + base.text_middle_point = Vector3::new(point.x, point.y, point.z); + base.text_user_positioned = true; + } + } + A::ReverseArrows => { + let base = self.base_mut(); + base.flip_arrow1 = !base.flip_arrow1; + base.flip_arrow2 = !base.flip_arrow2; + } + A::AboveDimLine if grip_id == text_grip => { + if let Some(point) = above_dimension_text_position(self) { + let base = self.base_mut(); + base.text_middle_point = Vector3::new(point.x, point.y + 1.0, point.z); + base.text_user_positioned = true; } } - // Stretch / Move-variants / Reverse Arrows / Rotate Text / - // Above Dim Line need either a follow-up drag or a numeric - // prompt — wired to default Stretch behaviour for now. _ => {} } } + + fn grip_menu_value_prompt( + &self, + grip_id: usize, + action: crate::scene::model::object::GripMenuAction, + ) -> Option<&'static str> { + use crate::scene::model::object::GripMenuAction as A; + let text_grip = match self { + Dimension::Linear(_) | Dimension::Aligned(_) => 3, + Dimension::Radius(_) | Dimension::Diameter(_) => 2, + Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => 4, + Dimension::Ordinate(_) => 3, + Dimension::Arc(d) => if d.has_leader { 6 } else { 4 }, + Dimension::LargeRadial(_) => 4, + }; + (grip_id == text_grip && matches!(action, A::RotateText)) + .then_some("Specify text rotation") + } + + fn apply_grip_menu_value( + &mut self, + grip_id: usize, + action: crate::scene::model::object::GripMenuAction, + value: f64, + ) { + use crate::scene::model::object::GripMenuAction as A; + if self.grip_menu_value_prompt(grip_id, action).is_some() + && matches!(action, A::RotateText) + { + self.base_mut().text_rotation = value.to_radians(); + } + } } // ── Tessellation ───────────────────────────────────────────────────────── @@ -1343,167 +1437,982 @@ 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. -pub fn style_sections(style: &DimStyle) -> 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() - } +/// 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 { + 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 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 (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() + } + }; + + let mut arrow_options: Vec = std::iter::once("Closed filled".to_string()) + .chain( + document + .block_records + .iter() + .map(|record| record.name.clone()) + .filter(|name| !name.is_empty()), + ) + .collect(); + let mut linetype_options: Vec = document + .line_types + .iter() + .map(|line_type| line_type.name.clone()) + .filter(|name| !name.is_empty()) + .collect(); + let text_style_options: Vec = document + .text_styles + .iter() + .map(|record| record.name.clone()) + .filter(|name| !name.is_empty()) + .collect(); + + 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()); + } + } + 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![ - ro(t!("Arrow size").as_ref(), "dim_arrow_size", f(s.dimasz)), - ro(t!("Dim line color").as_ref(), "dim_line_color", s.dimclrd.to_string()), - ro( + 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", - s.dimlwd.to_string(), + PropValue::Choice { + selected: lineweight_label(int(ov::DIMLWD, s.dimlwd)), + options: lineweight_options(), + }, ), - ro(t!("Ext line color").as_ref(), "dim_ext_line_color", s.dimclre.to_string()), - ro( + property( t!("Ext line lineweight").as_ref(), "dim_ext_line_lineweight", - s.dimlwe.to_string(), + PropValue::Choice { + selected: lineweight_label(int(ov::DIMLWE, s.dimlwe)), + options: lineweight_options(), + }, ), - 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!("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!("Dim line ext").as_ref(), "dim_line_ext", f(s.dimdle)), - 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)), - ro(t!("Ext line fixed").as_ref(), "dim_ext_line_fixed", yn(s.dimfxlon)), - ro( + 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", - f(s.dimfxl), + 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![ - 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()), + 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_text_override(dimension.base()) + .unwrap_or("") + .to_string(), + ), + ), ], }, 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)), + 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![ - 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( + 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", - yn(s.dimzin & 4 != 0), + yes(dimzin & 4 != 0), + &["Yes", "No"], + true, ), - ro( + choice( t!("Suppress trailing zeros").as_ref(), "dim_suppress_trailing_zeros", - yn(s.dimzin & 8 != 0), + 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(), + }, ), - 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( + 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", - yn(s.dimaltz & 4 != 0), + yes(dimaltz & 4 != 0), + &["Yes", "No"], + dimalt, ), - ro( + choice( t!("Alt suppress trailing zeros").as_ref(), "dim_alt_suppress_trailing_zeros", - yn(s.dimaltz & 8 != 0), + 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![ - 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( + 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", - s.dimtdec.to_string(), + 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()) + }, ), - ro( + 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", - f(s.dimtfac), + real(ov::DIMTFAC, s.dimtfac), + tolerance_enabled, ), - ro( + choice( t!("Tolerance pos vert").as_ref(), "dim_tolerance_pos_vert", - s.dimtolj.to_string(), + tolerance_vertical_label(int(ov::DIMTOLJ, s.dimtolj)), + &["Bottom", "Middle", "Top"], + tolerance_enabled, ), - ro( + 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", - yn(s.dimtzin & 4 != 0), + yes(dimtzin & 4 != 0), + &["Yes", "No"], + tolerance_enabled, ), - ro( + choice( t!("Tolerance suppress trailing zeros").as_ref(), "dim_tolerance_suppress_trailing_zeros", - yn(s.dimtzin & 8 != 0), + 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( + selected: &str, + options: &[&str], +) -> crate::scene::model::object::PropValue { + crate::scene::model::object::PropValue::Choice { + selected: selected.to_string(), + options: options.iter().map(|value| (*value).to_string()).collect(), + } +} + +fn block_name(document: &CadDocument, handle: acadrust::Handle, fallback: &str) -> String { + if handle.is_null() { + return "Closed filled".to_string(); + } + document + .block_records + .iter() + .find(|record| record.handle == handle) + .map(|record| record.name.clone()) + .unwrap_or_else(|| fallback.to_string()) +} + +fn linetype_name(document: &CadDocument, handle: acadrust::Handle) -> String { + document + .line_types + .iter() + .find(|line_type| line_type.handle == handle) + .map(|line_type| line_type.name.clone()) + .unwrap_or_else(|| "ByBlock".to_string()) +} + +fn linear_unit_label(value: i16) -> &'static str { + match value { + 1 => "Scientific", + 3 => "Engineering", + 4 => "Architectural", + 5 => "Fractional", + 6 => "Desktop", + _ => "Decimal", + } +} + use acadrust::{CadDocument, EntityType, Handle}; use crate::scene::convert::tess_util::aci_to_rgba; @@ -1736,6 +2645,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); @@ -1753,6 +2664,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); @@ -1792,6 +2704,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) { @@ -1912,12 +2830,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 @@ -1947,6 +2868,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 { @@ -1969,18 +2893,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. @@ -2265,6 +3180,50 @@ 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(), + pattern_stations: 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 @@ -2430,7 +3389,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; @@ -2476,6 +3435,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. @@ -2783,12 +3745,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; @@ -2814,14 +3786,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, @@ -3132,7 +4105,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. @@ -3194,6 +4169,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); @@ -3247,11 +4227,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); @@ -3262,6 +4243,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, @@ -3319,7 +4346,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). @@ -3347,21 +4374,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 { "" }; @@ -3369,18 +4397,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. @@ -3393,10 +4447,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; @@ -3450,7 +4513,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 { @@ -3482,7 +4545,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 { @@ -3497,6 +4565,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); @@ -3523,23 +4601,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. @@ -3548,15 +4642,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), } } @@ -3569,8 +4671,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 @@ -3581,7 +4683,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, @@ -3617,13 +4719,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 { @@ -3633,12 +4735,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 @@ -3810,6 +4918,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 @@ -3849,13 +4958,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; } } @@ -3882,10 +4998,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); @@ -3925,6 +5042,7 @@ fn dimension_text_pos_f64( text_w, arrow, dimtix, + dimatfit, dimtad, ) } @@ -3943,6 +5061,7 @@ fn dimension_text_pos_f64( text_w, arrow, dimtix, + dimatfit, dimtad, ) } @@ -4058,11 +5177,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); } @@ -4083,6 +5202,7 @@ mod dimtad_tests { 0.0, 1.0, false, + 3, 2, ); assert!( @@ -4102,6 +5222,7 @@ mod dimtad_tests { 0.0, 1.0, false, + 3, 2, ); assert!( @@ -4121,31 +5242,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"); } } - diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index 68efd6668..a3768011c 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -1,6 +1,7 @@ use acadrust::entities::{Dimension, DimensionLinear}; use acadrust::types::Vector3; use acadrust::EntityType; +use cadkernel::geom2d::{closest_point, Curve, Line as KernelLine}; use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; @@ -57,6 +58,25 @@ enum Step { DimensionLine { first: DVec3, second: DVec3 }, } +#[derive(Clone, Copy)] +enum AxisMode { + Automatic, + Horizontal, + Vertical, + Rotated(f64), +} + +impl AxisMode { + fn axis(self, first: DVec3, second: DVec3, point: DVec3) -> DVec3 { + match self { + Self::Automatic => measure_axis(first, second, point), + Self::Horizontal => DVec3::X, + Self::Vertical => DVec3::Y, + Self::Rotated(angle) => DVec3::new(angle.cos(), angle.sin(), 0.0), + } + } +} + pub struct LinearDimensionCommand { step: Step, plane: WorkingPlane, @@ -68,6 +88,13 @@ pub struct LinearDimensionCommand { text_angle: Option, /// True while the next typed value is captured as the text angle. awaiting_angle: bool, + /// True while a rotation value for the Rotated option is being entered. + awaiting_rotation: bool, + axis_mode: AxisMode, + selecting_object: bool, + picked_entity: Option, + source_handle: Option, + mtext_override: bool, } impl LinearDimensionCommand { @@ -79,6 +106,12 @@ impl LinearDimensionCommand { awaiting_text: false, text_angle: None, awaiting_angle: false, + awaiting_rotation: false, + axis_mode: AxisMode::Automatic, + selecting_object: false, + picked_entity: None, + source_handle: None, + mtext_override: false, } } } @@ -94,18 +127,32 @@ impl CadCommand for LinearDimensionCommand { fn prompt(&self) -> String { if self.awaiting_text { - return t!("DIMLINEAR Enter dimension text (blank = measured value):").into_owned(); + return if self.mtext_override { + t!("DIMLINEAR Enter formatted dimension text (blank = measured value):") + .into_owned() + } else { + t!("DIMLINEAR Enter dimension text (blank = measured value):").into_owned() + }; } if self.awaiting_angle { return t!("DIMLINEAR Specify text angle (degrees):").into_owned(); } + if self.awaiting_rotation { + return t!("DIMLINEAR Specify dimension line angle (degrees):").into_owned(); + } + if self.selecting_object { + return t!("DIMLINEAR Select object to dimension:").into_owned(); + } match self.step { - Step::FirstPoint => t!("DIMLINEAR Specify first extension line origin:").into_owned(), + Step::FirstPoint => { + t!("DIMLINEAR Specify first extension line origin or press Enter to select object:") + .into_owned() + } Step::SecondPoint(_) => { - t!("DIMLINEAR Specify second extension line origin [Text/Angle]:").into_owned() + t!("DIMLINEAR Specify second extension line origin:").into_owned() } Step::DimensionLine { .. } => { - t!("DIMLINEAR Specify dimension line location [Text/Angle]:").into_owned() + t!("DIMLINEAR Specify dimension line location [Mtext/Text/Angle/Horizontal/Vertical/Rotated]:").into_owned() } } } @@ -117,6 +164,9 @@ impl CadCommand for LinearDimensionCommand { CmdResult::NeedPoint } Step::SecondPoint(first) => { + if pt.distance_squared(first) <= 1e-24 { + return CmdResult::NeedPoint; + } self.step = Step::DimensionLine { first, second: pt }; CmdResult::NeedPoint } @@ -125,21 +175,29 @@ impl CadCommand for LinearDimensionCommand { let second = self.plane.to_local(second); let pt = self.plane.to_local(pt); let mut dim = DimensionLinear::new(v3(first), v3(second)); - let axis = measure_axis(first, second, pt); + let axis = self.axis_mode.axis(first, second, pt); dim.rotation = axis.y.atan2(axis.x); - dim.definition_point = v3(pt); - dim.base.definition_point = v3(pt); + dim.set_offset(dimension_line_offset(second, pt, axis)); + dim.base.definition_point = dim.definition_point; dim.base.text_middle_point = v3(linear_text_pos(first, second, pt, axis)); dim.base.insertion_point = dim.base.text_middle_point; dim.base.actual_measurement = dim.measurement(); - dim.base.user_text = self.text_override.clone(); + crate::entities::dimension::set_dimension_text_override( + &mut dim.base, + self.text_override.clone(), + ); // An explicit text angle overrides the UCS-derived rotation. if let Some(a) = self.text_angle { dim.base.text_rotation = a; } - CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension( + let entity = self.plane.place_entity(EntityType::Dimension( Dimension::Linear(dim), - ))) + )); + if let Some(source) = self.source_handle { + CmdResult::CommitAssociativeDimension { entity, source } + } else { + CmdResult::CommitAndExit(entity) + } } } } @@ -154,6 +212,14 @@ impl CadCommand for LinearDimensionCommand { self.awaiting_angle = false; return CmdResult::NeedPoint; } + if self.awaiting_rotation { + self.awaiting_rotation = false; + return CmdResult::NeedPoint; + } + if matches!(self.step, Step::FirstPoint) { + self.selecting_object = true; + return CmdResult::NeedPoint; + } CmdResult::Cancel } @@ -168,7 +234,7 @@ impl CadCommand for LinearDimensionCommand { fn point_step_accepts_keywords(&self) -> bool { // While typing the override text or angle, route input as a value, not // a point pick / keyword. - !self.awaiting_text && !self.awaiting_angle + !self.awaiting_text && !self.awaiting_angle && !self.awaiting_rotation } fn wants_text_with_spaces(&self) -> bool { @@ -199,8 +265,24 @@ impl CadCommand for LinearDimensionCommand { self.awaiting_angle = false; return Some(CmdResult::NeedPoint); } + if self.awaiting_rotation { + if let Some(angle) = crate::entities::common::parse_typed_angle(text.trim()) { + self.axis_mode = AxisMode::Rotated(angle); + } + self.awaiting_rotation = false; + return Some(CmdResult::NeedPoint); + } + if !matches!(self.step, Step::DimensionLine { .. }) { + return None; + } match text.trim().to_uppercase().as_str() { - "T" | "TEXT" | "M" | "MTEXT" => { + "T" | "TEXT" => { + self.mtext_override = false; + self.awaiting_text = true; + Some(CmdResult::NeedPoint) + } + "M" | "MTEXT" => { + self.mtext_override = true; self.awaiting_text = true; Some(CmdResult::NeedPoint) } @@ -208,10 +290,54 @@ impl CadCommand for LinearDimensionCommand { self.awaiting_angle = true; Some(CmdResult::NeedPoint) } + "H" | "HORIZONTAL" => { + self.axis_mode = AxisMode::Horizontal; + Some(CmdResult::NeedPoint) + } + "V" | "VERTICAL" => { + self.axis_mode = AxisMode::Vertical; + Some(CmdResult::NeedPoint) + } + "R" | "ROTATED" => { + self.awaiting_rotation = true; + Some(CmdResult::NeedPoint) + } _ => None, } } + fn needs_entity_pick(&self) -> bool { + self.selecting_object + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn inject_before_entity_pick(&self) -> bool { + true + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.picked_entity = Some(entity); + } + + fn on_entity_pick(&mut self, handle: acadrust::Handle, point: DVec3) -> CmdResult { + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + let Some((first, second)) = dimension_source_points(&entity, point) else { + return CmdResult::NeedPoint; + }; + if first.distance_squared(second) <= 1e-24 { + return CmdResult::NeedPoint; + } + self.source_handle = Some(handle); + self.selecting_object = false; + self.step = Step::DimensionLine { first, second }; + CmdResult::NeedPoint + } + fn on_mouse_move(&mut self, pt: DVec3) -> Option { match self.step { Step::FirstPoint => None, @@ -220,7 +346,7 @@ impl CadCommand for LinearDimensionCommand { let first = self.plane.to_local(first); let second = self.plane.to_local(second); let pt = self.plane.to_local(pt); - let axis = measure_axis(first, second, pt); + let axis = self.axis_mode.axis(first, second, pt); let points = linear_dimension_preview(first, second, pt, axis) .into_iter() .map(|point| self.plane.to_world(point)) @@ -235,6 +361,97 @@ fn v3(pt: DVec3) -> Vector3 { Vector3::new(pt.x, pt.y, pt.z) } +fn dimension_line_offset(second: DVec3, point: DVec3, axis: DVec3) -> f64 { + let perpendicular = DVec3::new(-axis.y, axis.x, 0.0); + (point - second).dot(perpendicular) +} + +fn dimension_source_points(entity: &EntityType, click: DVec3) -> Option<(DVec3, DVec3)> { + let point = |p: Vector3| DVec3::new(p.x, p.y, p.z); + match entity { + EntityType::Line(line) => Some((point(line.start), point(line.end))), + EntityType::Arc(arc) => Some((point(arc.start_point_wcs()), point(arc.end_point_wcs()))), + EntityType::LwPolyline(polyline) => nearest_planar_source( + polyline + .vertices + .iter() + .map(|vertex| [vertex.location.x, vertex.location.y]), + polyline.is_closed, + polyline.elevation, + polyline.normal, + click, + ), + EntityType::Polyline2D(polyline) => { + let vertices = crate::entities::polyline::drawn_vertices2d(polyline) + .unwrap_or_else(|| polyline.vertices.clone()); + nearest_planar_source( + vertices + .iter() + .map(|vertex| [vertex.location.x, vertex.location.y]), + polyline.is_closed(), + polyline.elevation, + polyline.normal, + click, + ) + } + _ => None, + } +} + +fn nearest_planar_source( + points: impl IntoIterator, + closed: bool, + elevation: f64, + normal: Vector3, + click: DVec3, +) -> Option<(DVec3, DVec3)> { + let click = crate::scene::view::transform::wcs_point_to_ocs( + (click.x, click.y, click.z), + (normal.x, normal.y, normal.z), + ); + let (first, second) = nearest_segment(points, closed, [click.0, click.1])?; + Some(( + ocs_point(first, elevation, normal), + ocs_point(second, elevation, normal), + )) +} + +fn nearest_segment( + points: impl IntoIterator, + closed: bool, + click: [f64; 2], +) -> Option<([f64; 2], [f64; 2])> { + let points: Vec<_> = points.into_iter().collect(); + if points.len() < 2 { + return None; + } + let count = if closed { points.len() } else { points.len() - 1 }; + (0..count) + .map(|index| { + let first = points[index]; + let second = points[(index + 1) % points.len()]; + let distance = closest_point( + &Curve::Line(KernelLine { + start: first, + end: second, + }), + click, + ) + .distance; + (distance, first, second) + }) + .min_by(|a, b| a.0.total_cmp(&b.0)) + .map(|(_, first, second)| (first, second)) +} + +fn ocs_point(point: [f64; 2], elevation: f64, normal: Vector3) -> DVec3 { + let point = crate::scene::view::transform::ocs_point_to_wcs( + (point[0], point[1], elevation), + (normal.x, normal.y, normal.z), + ); + DVec3::new(point.0, point.1, point.2) +} + fn preview_wire(points: Vec) -> WireModel { WireModel { point_marker: None, @@ -290,7 +507,23 @@ fn dim_line_endpoints(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> ( fn linear_dimension_preview(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> Vec { let (d1, d2) = dim_line_endpoints(first, second, def, axis); let nan = DVec3::new(f64::NAN, f64::NAN, f64::NAN); - vec![first, d1, nan, second, d2, nan, d1, d2] + let arrow = 0.22; + let perp = DVec3::new(-axis.y, axis.x, 0.0); + let text = linear_text_pos(first, second, def, axis); + let half_width = ((second - first).length().log10().max(0.0) + 1.0) * 0.18; + let half_height = 0.16; + vec![ + first, d1, nan, second, d2, nan, d1, d2, nan, + d1, d1 + axis * arrow + perp * arrow * 0.45, nan, + d1, d1 + axis * arrow - perp * arrow * 0.45, nan, + d2, d2 - axis * arrow + perp * arrow * 0.45, nan, + d2, d2 - axis * arrow - perp * arrow * 0.45, nan, + text - axis * half_width - perp * half_height, + text + axis * half_width - perp * half_height, + text + axis * half_width + perp * half_height, + text - axis * half_width + perp * half_height, + text - axis * half_width - perp * half_height, + ] } fn linear_text_pos(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> DVec3 { diff --git a/src/scene/dimension_assoc.rs b/src/scene/dimension_assoc.rs new file mode 100644 index 000000000..bda8f168e --- /dev/null +++ b/src/scene/dimension_assoc.rs @@ -0,0 +1,250 @@ +use acadrust::entities::Dimension; +use acadrust::objects::{ + AssocDimensionAssociation, AssocDimensionReference, AssociativeData, + AssociativeObject, ObjectType, +}; +use acadrust::types::{Handle, Vector3}; +use acadrust::EntityType; + +use super::{ChangeKind, Scene}; + +fn point_distance_squared(first: Vector3, second: Vector3) -> f64 { + let dx = first.x - second.x; + let dy = first.y - second.y; + let dz = first.z - second.z; + dx * dx + dy * dy + dz * dz +} + +fn ocs_point(x: f64, y: f64, elevation: f64, normal: Vector3) -> Vector3 { + let point = crate::scene::view::transform::ocs_point_to_wcs( + (x, y, elevation), + (normal.x, normal.y, normal.z), + ); + Vector3::new(point.0, point.1, point.2) +} + +fn source_points(entity: &EntityType) -> Vec { + match entity { + EntityType::Line(line) => vec![line.start, line.end], + EntityType::Arc(arc) => vec![arc.start_point_wcs(), arc.end_point_wcs()], + EntityType::LwPolyline(polyline) => polyline + .vertices + .iter() + .map(|vertex| { + ocs_point( + vertex.location.x, + vertex.location.y, + polyline.elevation, + polyline.normal, + ) + }) + .collect(), + EntityType::Polyline2D(polyline) => polyline + .vertices + .iter() + .map(|vertex| { + ocs_point( + vertex.location.x, + vertex.location.y, + polyline.elevation, + polyline.normal, + ) + }) + .collect(), + _ => Vec::new(), + } +} + +fn source_marker(entity: &EntityType, point: Vector3) -> Option { + source_points(entity) + .into_iter() + .enumerate() + .min_by(|(_, first), (_, second)| { + point_distance_squared(*first, point) + .total_cmp(&point_distance_squared(*second, point)) + }) + .map(|(index, _)| index as i32) +} + +fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Option { + let source = *reference.xrefs.first()?; + let entity = scene.document.get_entity(source)?; + source_points(entity) + .get(reference.main_gs_marker.max(0) as usize) + .copied() +} + +pub(crate) fn dimension_is_associative( + document: &acadrust::CadDocument, + dimension: Handle, +) -> bool { + document.objects.values().any(|object| { + let ObjectType::Associative(object) = object else { + return false; + }; + let AssociativeData::DimensionAssociation(association) = &object.data else { + return false; + }; + association.dimension == dimension + && association.associativity != 0 + && association.references.iter().flatten().any(|reference| { + reference + .xrefs + .iter() + .any(|source| document.get_entity(*source).is_some()) + }) + }) +} + +impl Scene { + pub(crate) fn attach_linear_dimension_association( + &mut self, + dimension: Handle, + sources: [Option; 2], + ) { + let Some(EntityType::Dimension(Dimension::Linear(linear))) = + self.document.get_entity(dimension) + else { + return; + }; + let first_point = linear.first_point; + let second_point = linear.second_point; + let source_data = [first_point, second_point].map(|point| point); + let resolved: [Option<(Handle, i32)>; 2] = std::array::from_fn(|index| { + let source = sources[index]?; + let entity = self.document.get_entity(source)?; + source_marker(entity, source_data[index]).map(|marker| (source, marker)) + }); + if resolved.iter().all(Option::is_none) { + return; + } + + let reference = |source: Handle, marker: i32, point: Vector3| AssocDimensionReference { + class_name: "AcDbOsnapPointRef".to_string(), + osnap_type: 1, + xrefs: vec![source], + main_subent_type: 1, + main_gs_marker: marker, + osnap_point: point, + ..AssocDimensionReference::default() + }; + let mut references: [Vec; 4] = + std::array::from_fn(|_| Vec::new()); + let mut associativity = 0; + for (index, resolved) in resolved.into_iter().enumerate() { + if let Some((source, marker)) = resolved { + associativity |= 1 << index; + references[index].push(reference(source, marker, source_data[index])); + } + } + + let association_handle = self.document.allocate_handle(); + let mut object = AssociativeObject::new("DIMASSOC", "AcDbDimAssoc"); + object.handle = association_handle; + object.reactors.push(dimension); + object.data = AssociativeData::DimensionAssociation(AssocDimensionAssociation { + associativity, + dimension, + references, + ..AssocDimensionAssociation::default() + }); + self.document + .objects + .insert(association_handle, ObjectType::Associative(object)); + + let mut reactor_targets = vec![dimension]; + reactor_targets.extend(sources.into_iter().flatten()); + reactor_targets.sort_by_key(|handle| handle.value()); + reactor_targets.dedup(); + for handle in reactor_targets { + if let Some(entity) = self.document.get_entity_mut(handle) { + if !entity.common().reactors.contains(&association_handle) { + entity.common_mut().reactors.push(association_handle); + } + } + } + } + + pub(crate) fn infer_linear_dimension_sources( + &self, + dimension: Handle, + ) -> [Option; 2] { + let Some(EntityType::Dimension(Dimension::Linear(linear))) = + self.document.get_entity(dimension) + else { + return [None, None]; + }; + [linear.first_point, linear.second_point].map(|point| { + self.document + .entities() + .filter(|entity| entity.common().handle != dimension) + .filter_map(|entity| { + source_points(entity) + .into_iter() + .map(|candidate| point_distance_squared(candidate, point)) + .min_by(f64::total_cmp) + .map(|distance| (distance, entity.common().handle)) + }) + .filter(|(distance, _)| *distance <= 1e-16) + .min_by(|first, second| first.0.total_cmp(&second.0)) + .map(|(_, handle)| handle) + }) + } + + pub(crate) fn refresh_associative_dimensions( + &mut self, + changes: &[(Handle, ChangeKind)], + ) -> Vec<(Handle, ChangeKind)> { + let changed: rustc_hash::FxHashSet<_> = + changes.iter().map(|(handle, _)| *handle).collect(); + if changed.is_empty() { + return Vec::new(); + } + let associations: Vec<_> = self + .document + .objects + .values() + .filter_map(|object| { + let ObjectType::Associative(object) = object else { + return None; + }; + let AssociativeData::DimensionAssociation(association) = &object.data else { + return None; + }; + association + .references + .iter() + .flatten() + .any(|reference| reference.xrefs.iter().any(|handle| changed.contains(handle))) + .then_some(association.clone()) + }) + .collect(); + + let mut refreshed = Vec::new(); + for association in associations { + let first = association.references[0] + .first() + .and_then(|reference| resolve_reference(self, reference)); + let second = association.references[1] + .first() + .and_then(|reference| resolve_reference(self, reference)); + if first.is_none() && second.is_none() { + continue; + } + if let Some(EntityType::Dimension(Dimension::Linear(linear))) = + self.document.get_entity_mut(association.dimension) + { + if let Some(first) = first { + linear.first_point = first; + } + if let Some(second) = second { + linear.second_point = second; + } + linear.base.actual_measurement = linear.measurement(); + linear.base.definition_point = linear.definition_point; + refreshed.push((association.dimension, ChangeKind::Modified)); + } + } + refreshed + } +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index b22dde385..cf14edc61 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -22,6 +22,7 @@ pub mod view; mod boundary; mod camera_ops; pub(crate) mod centerline; +pub(crate) mod dimension_assoc; pub(crate) mod centermark; mod entity; mod group_layer; @@ -2400,6 +2401,11 @@ impl Scene { changes.push(change); } } + for change in self.refresh_associative_dimensions(&changes) { + if !changes.iter().any(|(handle, _)| *handle == change.0) { + changes.push(change); + } + } for change in self.refresh_associative_hatches(&changes) { if !changes.iter().any(|(handle, _)| *handle == change.0) { changes.push(change); diff --git a/src/ui/color_select.rs b/src/ui/color_select.rs index 233ec87e4..0bc203cc4 100644 --- a/src/ui/color_select.rs +++ b/src/ui/color_select.rs @@ -16,6 +16,8 @@ use crate::t; pub struct ColorExtras { pub by_layer: bool, pub by_block: bool, + pub none: bool, + pub background: bool, } /// Encode a colour as the ACI integer string the style editors store @@ -38,7 +40,7 @@ pub fn iced_to_acad_color(color: Color) -> AcadColor { AcadColor::Rgb { r, g, b } } -/// Return the closest AutoCAD Color Index for an RGB colour. +/// Return the closest CAD Color Index for an RGB colour. pub fn nearest_aci(r: u8, g: u8, b: u8) -> u8 { let mut best = 7; let mut best_distance = u32::MAX; @@ -209,7 +211,27 @@ pub fn color_list<'a>( .into() }; + let logical_row = |color: AcadColor, label: &'static str| -> Element<'a, Message> { + let (bg, _) = acad_color_display(color); + button( + row![swatch(bg), text(t!(label)).size(11)] + .spacing(5) + .align_y(iced::Center), + ) + .on_press(on_select(color)) + .style(list_row_style) + .padding([2, 4]) + .width(Length::Fill) + .into() + }; + let mut list = column![].spacing(1); + if extras.none { + list = list.push(named_row(AcadColor::None)); + } + if extras.background { + list = list.push(logical_row(AcadColor::ByBlock, "Background")); + } if extras.by_layer { list = list.push(named_row(AcadColor::ByLayer)); } @@ -297,7 +319,7 @@ pub fn index_color_page<'a>( .into() }; - // AutoCAD's first standard indexed colours. + // The first standard indexed CAD colours. let mut standard = row![].spacing(4); for idx in 1u8..=9 { standard = standard.push(swatch_button(AcadColor::Index(idx), 22.0)); @@ -402,7 +424,7 @@ pub fn index_color_page<'a>( tabs, text("Standard colors").size(11), standard, - text("AutoCAD Color Index (ACI) 10–249").size(11), + text("CAD Color Index (ACI) 10–249").size(11), text("Color family → · Shade / intensity ↓") .size(9) diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 1a036f4cb..5d562dd45 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -834,6 +834,7 @@ impl PropertiesPanel { crate::ui::color_select::ColorExtras { by_layer: true, by_block: true, + ..Default::default() }, Message::PropBgColorChanged, Message::PropBgColorPickerToggle, @@ -851,18 +852,35 @@ impl PropertiesPanel { // entity's main colour. Used by hatch gradient colours and the dim-line // colour override (Leader / Dimension). Dim colours legitimately take // ByLayer / ByBlock; gradient colours do not. - if field == "gradient_color_1" || field == "gradient_color_2" || field == "dim_line_color" { + if matches!( + field, + "gradient_color_1" + | "gradient_color_2" + | "dim_line_color" + | "dim_ext_line_color" + | "dim_text_color" + | "dim_text_fill_color" + ) { let open = self.open_color_field.as_deref() == Some(field); let fsel = field.to_string(); - let extras = if field == "dim_line_color" { + let full_palette_field = field.to_string(); + let extras = if field == "dim_text_fill_color" { + crate::ui::color_select::ColorExtras { + none: true, + background: true, + ..Default::default() + } + } else if field.starts_with("dim_") { crate::ui::color_select::ColorExtras { by_layer: true, by_block: true, + ..Default::default() } } else { crate::ui::color_select::ColorExtras { by_layer: false, by_block: false, + ..Default::default() } }; let selector = crate::ui::color_select::color_selector( @@ -874,7 +892,10 @@ impl PropertiesPanel { color: c, }, Message::PropColorFieldToggle(field.to_string()), - Message::PropColorFieldToggle(field.to_string()), + Message::OpenColorWindow( + crate::app::ColorPickTarget::PropertiesField(full_palette_field), + color, + ), ); return prop_row_widget(label, selector); } @@ -884,6 +905,7 @@ impl PropertiesPanel { crate::ui::color_select::ColorExtras { by_layer: true, by_block: true, + ..Default::default() }, Message::PropColorChanged, Message::PropColorPickerToggle, diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index abcccd86c..9af9ff610 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -1135,6 +1135,7 @@ impl Ribbon { crate::ui::color_select::ColorExtras { by_layer: true, by_block: true, + ..Default::default() }, Message::RibbonColorChanged, Message::OpenColorWindow( diff --git a/src/ui/style/dimstyle.rs b/src/ui/style/dimstyle.rs index e6a7a2005..dc2db0c6a 100644 --- a/src/ui/style/dimstyle.rs +++ b/src/ui/style/dimstyle.rs @@ -518,6 +518,7 @@ pub fn view_window<'a>( crate::ui::color_select::ColorExtras { by_layer: true, by_block: true, + ..Default::default() }, move |c| Message::DsEdit(f_sel.clone(), crate::ui::color_select::color_to_aci_string(c)), Message::DsColorMore(fld.clone()), diff --git a/src/ui/style/mleaderstyle.rs b/src/ui/style/mleaderstyle.rs index 0de3aee71..4b7e5a2a0 100644 --- a/src/ui/style/mleaderstyle.rs +++ b/src/ui/style/mleaderstyle.rs @@ -115,7 +115,11 @@ fn color_row<'a>( let selector = crate::ui::color_select::color_selector( current, open, - crate::ui::color_select::ColorExtras { by_layer: true, by_block: true }, + crate::ui::color_select::ColorExtras { + by_layer: true, + by_block: true, + ..Default::default() + }, move |color| Message::MLeaderStyleEdit { field, value: crate::ui::color_select::color_to_aci_string(color), diff --git a/src/ui/style/tablestyle.rs b/src/ui/style/tablestyle.rs index ce067b57a..4117ec778 100644 --- a/src/ui/style/tablestyle.rs +++ b/src/ui/style/tablestyle.rs @@ -317,7 +317,11 @@ fn cell_editor<'a>(v: &TableStyleView<'a>, row_index: u8) -> Element<'a, Message let selector = crate::ui::color_select::color_selector( current, v.color_open == Some((row_index, field)), - crate::ui::color_select::ColorExtras { by_layer: true, by_block: true }, + crate::ui::color_select::ColorExtras { + by_layer: true, + by_block: true, + ..Default::default() + }, move |color| Message::TableStyleCellEdit { row: row_index, field, diff --git a/src/ui/window/layer_state_manager.rs b/src/ui/window/layer_state_manager.rs index 00429e3f0..97f54f8b0 100644 --- a/src/ui/window/layer_state_manager.rs +++ b/src/ui/window/layer_state_manager.rs @@ -412,6 +412,7 @@ fn editor_layer_row<'a>( crate::ui::color_select::ColorExtras { by_layer: false, by_block: false, + ..Default::default() }, move |color| Message::LayerStateEditorLayerColor(index, color), Message::LayerStateEditorLayerColorToggle(index), diff --git a/src/ui/window/layers.rs b/src/ui/window/layers.rs index cffcfa4ab..11253ee51 100644 --- a/src/ui/window/layers.rs +++ b/src/ui/window/layers.rs @@ -813,6 +813,7 @@ fn layer_row<'a>( crate::ui::color_select::ColorExtras { by_layer: false, by_block: false, + ..Default::default() }, Message::LayerColorSet, Message::LayerColorPickerToggle(index),