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