From 2da520de80af23816d5ffab2f456118e384192dd Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Thu, 13 Aug 2026 09:46:56 -0300 Subject: [PATCH] fix: keep dimension text grips aligned during stretch --- src/app/properties.rs | 43 ++++++++ src/entities/dimension.rs | 227 +++++++++++++++++++++++++++++++++++++- 2 files changed, 268 insertions(+), 2 deletions(-) diff --git a/src/app/properties.rs b/src/app/properties.rs index 15c1e93b..b233c561 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1618,6 +1618,49 @@ impl OpenCADStudio { annotation_scale_handle, ); let mut entity_grips = dispatch::grips(contextual.as_ref()); + // Dimension::grips() cannot see the document, so an automatic dimension + // text grip cannot resolve its real DIMSTYLE/annotation-scaled position + // there. Correct it here, where both the document and displayed annotation + // scale are available. + if let acadrust::EntityType::Dimension(dim) = contextual.as_ref() { + if matches!( + dim, + acadrust::entities::Dimension::Linear(_) + | acadrust::entities::Dimension::Aligned(_) + ) && !dim.base().text_user_positioned + { + let anno_scale = annotation_scale_handle + .and_then(|handle| { + match self.tabs[i].scene.document.objects.get(&handle) { + Some(acadrust::objects::ObjectType::Scale(scale)) => Some( + scale.inverse_factor() + / self.tabs[i].scene.annotation_scale_unit_factor(), + ), + _ => None, + } + }) + .unwrap_or(self.tabs[i].scene.annotation_scale as f64); + + if let Some(position) = + crate::entities::dimension::dimension_text_grip_position( + dim, + &self.tabs[i].scene.document, + anno_scale, + ) + { + // The text grip is the final native grip of Linear/Aligned dims. + // While the text is still automatic, make this a point/stretch grip + // rather than a midpoint-translate grip. That makes the first drag use + // the displayed automatic position as its absolute starting point instead + // of translating the stale DWG text_middle_point. + if let Some(text_grip) = entity_grips.last_mut() { + text_grip.world = + glam::DVec3::new(position.x, position.y, position.z); + text_grip.is_midpoint = false; + } + } + } + } entity_grips.extend(crate::scene::model::solid_history::primitive_grips( &self.tabs[i].scene.document, handle, diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 998b8327..204942df 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -853,6 +853,184 @@ fn apply_to_v3(target: &mut acadrust::types::Vector3, apply: &GripApply) { } } + + +#[derive(Clone, Copy)] +struct DimTextRelativePosition { + along_fraction: f64, + perpendicular_offset: f64, + z_offset: f64, +} + +fn capture_dim_text_relative_position(dim: &Dimension) -> Option { + let base = dim.base(); + + // Auto-positioned text is recomputed by the renderer from DIMSTYLE. + // Only an explicitly moved text point needs to follow a grip deformation. + if !base.text_user_positioned { + return None; + } + + let text = base.text_middle_point; + if text.x * text.x + text.y * text.y + text.z * text.z <= 1e-16 { + return None; + } + + let (first, second, defpt, ax, ay) = match dim { + Dimension::Linear(d) => ( + d.first_point, + d.second_point, + d.definition_point, + 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 len = (dx * dx + dy * dy).sqrt(); + if len <= 1e-12 { + return None; + } + + ( + d.first_point, + d.second_point, + d.definition_point, + dx / len, + dy / len, + ) + } + _ => return None, + }; + + let px = -ay; + let py = ax; + + let t1 = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay; + let t2 = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay; + let text_t = (text.x - defpt.x) * ax + (text.y - defpt.y) * ay; + + let span = t2 - t1; + let along_fraction = if span.abs() > 1e-12 { + (text_t - t1) / span + } else { + 0.5 + }; + + let along = t1 + span * along_fraction; + let line_x = defpt.x + ax * along; + let line_y = defpt.y + ay * along; + + let perpendicular_offset = + (text.x - line_x) * px + (text.y - line_y) * py; + + Some(DimTextRelativePosition { + along_fraction, + perpendicular_offset, + z_offset: text.z - defpt.z, + }) +} + +fn restore_dim_text_relative_position( + dim: &mut Dimension, + saved: DimTextRelativePosition, +) { + let (first, second, defpt, ax, ay) = match dim { + Dimension::Linear(d) => ( + d.first_point, + d.second_point, + d.definition_point, + 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 len = (dx * dx + dy * dy).sqrt(); + if len <= 1e-12 { + return; + } + + ( + d.first_point, + d.second_point, + d.definition_point, + dx / len, + dy / len, + ) + } + _ => return, + }; + + let px = -ay; + let py = ax; + + let t1 = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay; + let t2 = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay; + + let along = t1 + (t2 - t1) * saved.along_fraction; + + let base = dim.base_mut(); + base.text_middle_point = Vector3::new( + defpt.x + ax * along + px * saved.perpendicular_offset, + defpt.y + ay * along + py * saved.perpendicular_offset, + defpt.z + saved.z_offset, + ); +} + +fn dimension_line_grip_position(dim: &Dimension) -> Option { + let (first, second, defpt, ax, ay) = match dim { + Dimension::Linear(d) => ( + d.first_point, + d.second_point, + d.definition_point, + 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 len = (dx * dx + dy * dy).sqrt(); + + if len <= 1e-12 { + return None; + } + + ( + d.first_point, + d.second_point, + d.definition_point, + dx / len, + dy / len, + ) + } + _ => return None, + }; + + let px = -ay; + let py = ax; + + // Project both extension origins onto the current dimension line. + let off1 = + (defpt.x - first.x) * px + (defpt.y - first.y) * py; + let off2 = + (defpt.x - second.x) * px + (defpt.y - second.y) * py; + + let p1 = DVec3::new( + first.x + px * off1, + first.y + py * off1, + defpt.z, + ); + + let p2 = DVec3::new( + second.x + px * off2, + second.y + py * off2, + defpt.z, + ); + + Some((p1 + p2) * 0.5) +} + impl Grippable for Dimension { fn grips(&self) -> Vec { // Auto-placed dimensions carry a zero text_middle_point sentinel; put @@ -870,13 +1048,21 @@ impl Grippable for Dimension { Dimension::Linear(d) => vec![ square_grip(0, dv3(&d.first_point)), center_grip(1, dv3(&d.second_point)), - center_grip(2, dv3(&d.definition_point)), + center_grip( + 2, + dimension_line_grip_position(self) + .unwrap_or_else(|| dv3(&d.definition_point)), + ), center_grip(3, text), ], Dimension::Aligned(d) => vec![ square_grip(0, dv3(&d.first_point)), center_grip(1, dv3(&d.second_point)), - center_grip(2, dv3(&d.definition_point)), + center_grip( + 2, + dimension_line_grip_position(self) + .unwrap_or_else(|| dv3(&d.definition_point)), + ), center_grip(3, text), ], Dimension::Radius(d) => vec![ @@ -935,6 +1121,8 @@ impl Grippable for Dimension { } } + + fn apply_grip(&mut self, grip_id: usize, apply: GripApply) { // Last grip always moves the text. let text_grip = match self { @@ -953,6 +1141,12 @@ impl Grippable for Dimension { return; } + // A manually positioned dimension text point is stored in the DWG as an + // absolute coordinate. Capture its relation to the old dimension geometry + // before moving a definition grip so that it can be reconstructed against + // the new geometry afterwards. + let relative_text = capture_dim_text_relative_position(self); + match self { Dimension::Linear(d) => match grip_id { 0 => apply_to_v3(&mut d.first_point, &apply), @@ -1013,6 +1207,11 @@ impl Grippable for Dimension { _ => {} }, } + + if let Some(saved) = relative_text { + restore_dim_text_relative_position(self, saved); + } + self.base_mut().actual_measurement = self.measurement(); } @@ -3800,6 +3999,30 @@ pub(crate) fn baked_dimension_text_entity( Some(ent) } +pub(crate) fn dimension_text_grip_position( + dim: &Dimension, + document: &CadDocument, + anno_scale: f64, +) -> Option { + // Once the user has moved the text, its saved point is authoritative. + let base = dim.base(); + if base.text_user_positioned { + let p = base.text_middle_point; + if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 { + return Some(p); + } + } + + // For automatic text, use the same text-building path used by the + // dimension picture so the grip follows the actual DIMSTYLE placement, + // including annotation scaling and fit behaviour. + match baked_dimension_text_entity(dim, document, anno_scale)? { + EntityType::Text(text) => Some(text.insertion_point), + EntityType::MText(text) => Some(text.insertion_point), + _ => None, + } +} + #[cfg(test)] mod dimtad_tests { use super::text_on_dim_line;