fix: keep dimension text grips aligned during stretch

This commit is contained in:
gianlucafiore 2026-08-13 09:46:56 -03:00
commit 2da520de80
2 changed files with 268 additions and 2 deletions

View file

@ -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,

View file

@ -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<DimTextRelativePosition> {
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<DVec3> {
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<GripDef> {
// 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<Vector3> {
// 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;