From 1f71a70580beccffde20d05a34993a9885f59d41 Mon Sep 17 00:00:00 2001 From: codallos Date: Wed, 16 Sep 2026 19:21:03 -0600 Subject: [PATCH] Fix diameter dimension grip and text alignment --- src/entities/dimension.rs | 110 +++++++++++++++++++++++++++++++---- src/scene/dimension_assoc.rs | 76 ++++++++++++++++++++++++ src/scene/modify.rs | 1 + 3 files changed, 177 insertions(+), 10 deletions(-) diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index ad3fa78db..c6a607405 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -930,6 +930,23 @@ fn capture_dim_text_relative_position(dim: &Dimension) -> Option { + let dx = d.definition_point.x - d.angle_vertex.x; + let dy = d.definition_point.y - d.angle_vertex.y; + let len = (dx * dx + dy * dy).sqrt(); + + if len <= 1e-12 { + return None; + } + + ( + d.angle_vertex, + d.definition_point, + d.angle_vertex, + dx / len, + dy / len, + ) + } _ => return None, }; @@ -989,6 +1006,23 @@ fn restore_dim_text_relative_position( dy / len, ) } + Dimension::Diameter(d) => { + let dx = d.definition_point.x - d.angle_vertex.x; + let dy = d.definition_point.y - d.angle_vertex.y; + let len = (dx * dx + dy * dy).sqrt(); + + if len <= 1e-12 { + return; + } + + ( + d.angle_vertex, + d.definition_point, + d.angle_vertex, + dx / len, + dy / len, + ) + } _ => return, }; @@ -1211,6 +1245,26 @@ impl Grippable for Dimension { }; if grip_id == text_grip { apply_to_v3(&mut self.base_mut().text_middle_point, &apply); + + // A diametric dimension must remain collinear when its text/leader + // grip is moved. Rotate both diameter endpoints around the fixed + // center so the leader and diameter share the same radial axis. + if let Dimension::Diameter(d) = self { + let center = d.center(); + let radius = d.measurement() * 0.5; + let offset = d.base.text_middle_point - center; + + if offset.length_squared() > 1e-24 && radius > 1e-12 { + let radial = offset.normalize() * radius; + + // Keep the endpoint nearest the text on the text side. + d.angle_vertex = center + radial; + d.definition_point = center - radial; + d.base.definition_point = d.definition_point; + d.base.actual_measurement = d.measurement(); + } + } + // Dragging the text grip pins it to a user-defined location, so it // no longer follows the style (DIMTAD). See #94. self.base_mut().text_user_positioned = true; @@ -4637,9 +4691,9 @@ fn dimension_geometry( &mut g, chord, far_chord, + lv(d.base.text_middle_point), arrow1, arrow2, - d.leader_length as f32, params, suppress, ); @@ -5025,9 +5079,9 @@ fn append_diameter_dimension( g: &mut DimGeom, chord: Vec3, far_chord: Vec3, + text_anchor: Vec3, arrow1: &ArrowKind, arrow2: &ArrowKind, - leader_length: f32, params: DimLineParams, suppress: SuppressFlags, ) { @@ -5036,9 +5090,14 @@ fn append_diameter_dimension( if diameter <= 1e-6 { return; } + // Text projected outside the diameter requires inward-pointing arrowheads. + let text_along = (params.text_position - chord).dot(axis); + let text_outside = text_along < 0.0 || text_along > diameter; let arrows_outside = if params.ticks || params.arrow_len <= 1e-6 { false + } else if text_outside { + true } else if diameter < 2.0 * params.arrow_len { true } else if diameter < params.text_width + 2.0 * params.arrow_len { @@ -5062,7 +5121,7 @@ fn append_diameter_dimension( .unwrap_or(line_length * 0.5); let split_point = first + axis * split; - let draw_inside_line = !arrows_outside || params.dimtofl; + let draw_inside_line = text_outside || !arrows_outside || params.dimtofl; if draw_inside_line && !suppress.dim1 && split > 1e-6 { add_segment_with_text_break(&mut g.dim_lines, first, split_point, params.text_break); } @@ -5087,18 +5146,25 @@ fn append_diameter_dimension( append_arrow(g, far_chord, -axis, arrow2); } - let text_along = (params.text_position - chord).dot(axis); if params.text_movement == 0 { - let (tip, suppressed) = if params.text_position.distance_squared(chord) + let (tip, direction, suppressed) = if params.text_position.distance_squared(chord) <= params.text_position.distance_squared(far_chord) { - (chord, suppress.dim1) + (chord, -axis, suppress.dim1) } else { - (far_chord, suppress.dim2) + (far_chord, axis, suppress.dim2) }; - if !suppressed && leader_length.abs() > 1e-6 { - let direction = normalized_or(params.text_position - tip, axis); - add_segment(&mut g.dim_lines, tip, tip + direction * leader_length.abs()); + if text_outside && !suppressed { + let leader_length = (text_anchor - tip).dot(direction); + + if leader_length > 1e-6 { + let text_extension = params.text_width * 0.5; + add_segment( + &mut g.dim_lines, + tip, + tip + direction * (leader_length + text_extension), + ); + } } else if text_along < 0.0 && !suppress.dim1 { add_segment(&mut g.dim_lines, chord, params.text_position); } else if text_along > diameter && !suppress.dim2 { @@ -6976,6 +7042,30 @@ fn dimension_text_pos_f64( p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 }; if use_saved { + if let Dimension::Diameter(d) = dim { + let dx = d.definition_point.x - d.angle_vertex.x; + let dy = d.definition_point.y - d.angle_vertex.y; + let len = (dx * dx + dy * dy).sqrt().max(1e-12); + + // Match the readable orientation used by text_on_dim_line(). + let (mut nx, mut ny) = (dx / len, dy / len); + if nx < 0.0 || (nx == 0.0 && ny < 0.0) { + nx = -nx; + ny = -ny; + } + + let px = -ny; + let py = nx; + + let perp_sign = if dimtad == 4 { -1.0 } else { 1.0 }; + + return Vector3::new( + base.text_middle_point.x + px * perp_off * perp_sign, + base.text_middle_point.y + py * perp_off * perp_sign, + base.text_middle_point.z, + ); + } + return base.text_middle_point; } diff --git a/src/scene/dimension_assoc.rs b/src/scene/dimension_assoc.rs index 3eb93accc..a37ac4eb1 100644 --- a/src/scene/dimension_assoc.rs +++ b/src/scene/dimension_assoc.rs @@ -2157,6 +2157,82 @@ impl Scene { } refreshed } + pub(crate) fn sync_diameter_association_angle(&mut self, dimension: Handle) { + let chord = match self.document.get_entity(dimension) { + Some(EntityType::Dimension(Dimension::Diameter(diameter))) => { + diameter.angle_vertex + } + _ => return, + }; + + let association_handle = self.document.objects.iter().find_map(|(handle, object)| { + let ObjectType::Associative(object) = object else { + return None; + }; + + let AssociativeData::DimensionAssociation(association) = &object.data else { + return None; + }; + + (association.dimension == dimension).then_some(*handle) + }); + + let Some(association_handle) = association_handle else { + return; + }; + + // Resolve the radial source while the association is borrowed immutably. + let angle = { + let Some(ObjectType::Associative(object)) = + self.document.objects.get(&association_handle) + else { + return; + }; + + let AssociativeData::DimensionAssociation(association) = &object.data else { + return; + }; + + let Some(reference) = association.references[0].first() else { + return; + }; + + let Ok(walked) = chain::walk_chain(&self.document, &reference.xrefs) else { + return; + }; + + let Some(entity) = self.document.get_entity(walked.entity) else { + return; + }; + + let Some(radial) = radial_source_for_marker(entity, reference.main_gs_marker) else { + return; + }; + + let Some(radial) = chain_map(self, &walked).map_radial(radial) else { + return; + }; + + radial.angle_at(dpoint(chord)) + }; + + let Some(ObjectType::Associative(object)) = + self.document.objects.get_mut(&association_handle) + else { + return; + }; + + let AssociativeData::DimensionAssociation(association) = &mut object.data else { + return; + }; + + let Some(reference) = association.references[0].first_mut() else { + return; + }; + + reference.osnap_distance = angle; + reference.osnap_point = chord; + } } /// Average displacement of the dimension's resolved definition points. diff --git a/src/scene/modify.rs b/src/scene/modify.rs index f91764067..b13a1f853 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -1399,6 +1399,7 @@ impl Scene { if let Some(entity) = self.document.get_entity_mut(handle) { view::dispatch::apply_grip(entity, grip_id, apply); } + self.sync_diameter_association_angle(handle); if let Some((annotation_handle, old_landing)) = leader_landing_before { let new_landing = self.document.get_entity(handle).and_then(|entity| { let EntityType::Leader(leader) = entity else {