fix(text): honor two-point geometry and grips

This commit is contained in:
ramox81 2026-08-26 09:50:31 +03:00
commit c00d08d40a
2 changed files with 134 additions and 33 deletions

View file

@ -1162,6 +1162,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
if matches!( if matches!(
item.action, item.action,
GripMenuAction::Stretch GripMenuAction::Stretch
| GripMenuAction::MoveWithText
| GripMenuAction::MoveWithDimLine | GripMenuAction::MoveWithDimLine
| GripMenuAction::MoveWithLeader | GripMenuAction::MoveWithLeader
| GripMenuAction::MoveIndependent | GripMenuAction::MoveIndependent
@ -1212,7 +1213,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
{ {
(crate::entities::multileader::MOVE_ALL_GRIP, true) (crate::entities::multileader::MOVE_ALL_GRIP, true)
} else { } else {
(popup.grip_id, if is_dimension { false } else { g.is_midpoint }) (
popup.grip_id,
matches!(item.action, GripMenuAction::MoveWithText)
|| (!is_dimension && g.is_midpoint),
)
}; };
self.tabs[i].active_grip = Some(GripEdit::single( self.tabs[i].active_grip = Some(GripEdit::single(
popup.handle, popup.handle,

View file

@ -43,7 +43,24 @@ pub(crate) fn sync_text_alignment_point(t: &mut Text) {
(HA::Left, VA::Baseline) (HA::Left, VA::Baseline)
); );
if needs_alignment_point { if needs_alignment_point {
if t.alignment_point.is_none() { if matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) {
let point = t.alignment_point.unwrap_or(t.insertion_point);
let dx = point.x - t.insertion_point.x;
let dy = point.y - t.insertion_point.y;
if dx.hypot(dy) <= 1.0e-9 {
let span = t.height.max(1.0e-6)
* t.width_factor.abs().max(0.01)
* t.value.chars().count().max(1) as f64
* 0.6;
t.alignment_point = Some(acadrust::types::Vector3::new(
t.insertion_point.x + t.rotation.cos() * span,
t.insertion_point.y + t.rotation.sin() * span,
t.insertion_point.z,
));
} else {
t.alignment_point = Some(point);
}
} else if t.alignment_point.is_none() {
t.alignment_point = Some(t.insertion_point); t.alignment_point = Some(t.insertion_point);
} }
} else { } else {
@ -51,6 +68,21 @@ pub(crate) fn sync_text_alignment_point(t: &mut Text) {
} }
} }
fn two_point_span(t: &Text) -> Option<(f64, f64)> {
if !matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) {
return None;
}
let point = t.alignment_point?;
let dx = point.x - t.insertion_point.x;
let dy = point.y - t.insertion_point.y;
let distance = dx.hypot(dy);
(distance > 1.0e-9).then(|| (distance, dy.atan2(dx)))
}
fn displayed_rotation(t: &Text) -> f64 {
two_point_span(t).map_or(t.rotation, |(_, angle)| angle)
}
/// Resolved placement of a TEXT run: the baseline-anchored run origin (WCS xy) /// Resolved placement of a TEXT run: the baseline-anchored run origin (WCS xy)
/// plus every parameter needed to lay the glyphs out. Shared by `to_render` (the /// plus every parameter needed to lay the glyphs out. Shared by `to_render` (the
/// stroke path) and the SDF-quad text collector so both place text identically. /// stroke path) and the SDF-quad text collector so both place text identically.
@ -152,10 +184,9 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
); );
let resolved_style = resolve_text_style(&t.style, document); let resolved_style = resolve_text_style(&t.style, document);
let font_name = resolved_style.font_name; let font_name = resolved_style.font_name;
// AutoCAD text geometry rule: the entity stores the FINAL width factor / // The entity stores the final width factor and oblique angle copied from
// oblique angle, copied from the style at creation and persisting through // its style at creation. Only fall back to the style when an omitted field
// style edits. Use it as-is. Only fall back to the style when the entity // was read as zero.
// value is missing (the parser reports 0.0 for default-omitted fields).
let base_wf = if t.width_factor.abs() > 1e-9 { let base_wf = if t.width_factor.abs() > 1e-9 {
(t.width_factor as f32).clamp(0.01, 100.0) (t.width_factor as f32).clamp(0.01, 100.0)
} else { } else {
@ -168,17 +199,43 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
// mirror, and XOR keeps a double mirror an involution. // mirror, and XOR keeps a double mirror an involution.
let eff_backward = resolved_style.is_backward ^ (t.generation_flags & 0x2 != 0); let eff_backward = resolved_style.is_backward ^ (t.generation_flags & 0x2 != 0);
let eff_upside = resolved_style.is_upside_down ^ (t.generation_flags & 0x4 != 0); let eff_upside = resolved_style.is_upside_down ^ (t.generation_flags & 0x4 != 0);
let width_factor = if eff_backward { -base_wf } else { base_wf }; let mut width_factor = if eff_backward { -base_wf } else { base_wf };
let rotation = if eff_upside {
t.rotation as f32 + std::f32::consts::PI
} else {
t.rotation as f32
};
let oblique_angle = if t.oblique_angle.abs() > 1e-9 { let oblique_angle = if t.oblique_angle.abs() > 1e-9 {
t.oblique_angle as f32 t.oblique_angle as f32
} else { } else {
resolved_style.oblique_angle resolved_style.oblique_angle
}; };
let value_for_bounds = resolve_dxf_special_chars(&t.value);
let mut height = t.height.max(1.0e-9) as f32;
let mut base_rotation = t.rotation as f32;
// Aligned and Fit are true two-point modes. Both derive their baseline
// direction from the endpoints. Aligned scales height uniformly; Fit keeps
// the height and changes only the horizontal factor.
if let Some((span, angle)) = two_point_span(t) {
base_rotation = angle as f32;
if let Some(base_bounds) = text_local_bounds(
&font_name,
&value_for_bounds,
height,
width_factor,
oblique_angle,
) {
if base_bounds.advance > 1.0e-6 {
let scale = (span as f32 / base_bounds.advance).max(1.0e-6);
if matches!(t.horizontal_alignment, HA::Aligned) {
height *= scale;
} else {
width_factor *= scale;
}
}
}
}
let rotation = if eff_upside {
base_rotation + std::f32::consts::PI
} else {
base_rotation
};
// Anchor stays f64: large coordinates (UTM etc.) lose ~0.5 units of // Anchor stays f64: large coordinates (UTM etc.) lose ~0.5 units of
// precision when cast to f32, which snaps text baselines onto a coarse // precision when cast to f32, which snaps text baselines onto a coarse
// grid and makes adjacent rows collide. Only the small local offsets // grid and makes adjacent rows collide. Only the small local offsets
@ -188,17 +245,16 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
&t.vertical_alignment, &t.vertical_alignment,
&t.alignment_point, &t.alignment_point,
) { ) {
(HA::Aligned | HA::Middle | HA::Fit, _, Some(a)) => [a.x, a.y], (HA::Aligned | HA::Fit, _, _) => [t.insertion_point.x, t.insertion_point.y],
(HA::Middle, _, Some(a)) => [a.x, a.y],
(HA::Center | HA::Right, _, Some(a)) => [a.x, a.y], (HA::Center | HA::Right, _, Some(a)) => [a.x, a.y],
(_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x, a.y], (_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x, a.y],
_ => [t.insertion_point.x, t.insertion_point.y], _ => [t.insertion_point.x, t.insertion_point.y],
}; };
// Strip %%u/%%o for bounds (they add no width); resolve %%d/%%c/%%p for correct advance.
let value_for_bounds = resolve_dxf_special_chars(&t.value);
let bounds = text_local_bounds( let bounds = text_local_bounds(
&font_name, &font_name,
&value_for_bounds, &value_for_bounds,
t.height as f32, height,
width_factor, width_factor,
oblique_angle, oblique_angle,
); );
@ -213,7 +269,8 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
let ax = match t.horizontal_alignment { let ax = match t.horizontal_alignment {
HA::Left => 0.0, HA::Left => 0.0,
HA::Center | HA::Middle => b.advance * 0.5 * sign, HA::Center | HA::Middle => b.advance * 0.5 * sign,
HA::Right | HA::Aligned | HA::Fit => b.advance * sign, HA::Right => b.advance * sign,
HA::Aligned | HA::Fit => 0.0,
}; };
// Vertical anchor uses the inked extent (cap / baseline geometry). // Vertical anchor uses the inked extent (cap / baseline geometry).
let ay = match t.vertical_alignment { let ay = match t.vertical_alignment {
@ -236,7 +293,7 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
]; ];
TextPlacement { TextPlacement {
origin, origin,
height: t.height as f32, height,
rotation, rotation,
width_factor, width_factor,
oblique_angle, oblique_angle,
@ -247,12 +304,19 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
} }
fn grips(t: &Text) -> Vec<GripDef> { fn grips(t: &Text) -> Vec<GripDef> {
let p = glam::DVec3::new( let insertion = glam::DVec3::new(
t.insertion_point.x, t.insertion_point.x,
t.insertion_point.y, t.insertion_point.y,
t.insertion_point.z, t.insertion_point.z,
); );
vec![square_grip(0, p)] let mut grips = vec![square_grip(0, insertion)];
if let Some(point) = t.alignment_point {
grips.push(square_grip(
1,
glam::DVec3::new(point.x, point.y, point.z),
));
}
grips
} }
fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> { fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
@ -263,6 +327,8 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
// both points are live. // both points are live.
let is_plain_left = matches!(t.horizontal_alignment, HA::Left) let is_plain_left = matches!(t.horizontal_alignment, HA::Left)
&& matches!(t.vertical_alignment, VA::Baseline); && matches!(t.vertical_alignment, VA::Baseline);
let is_aligned = matches!(t.horizontal_alignment, HA::Aligned);
let is_two_point = matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
let pos_editable = is_plain_left || matches!(t.horizontal_alignment, HA::Aligned | HA::Fit); let pos_editable = is_plain_left || matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
let align_editable = !is_plain_left; let align_editable = !is_plain_left;
// The alignment point is meaningless (reset to the origin) for plain-Left text. // The alignment point is meaningless (reset to the origin) for plain-Left text.
@ -331,9 +397,18 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
t!("Height").as_ref(), t!("Height").as_ref(),
"height", "height",
t.height, t.height,
crate::entities::common::style_fixed_height(&t.style).is_none(), !is_aligned
&& crate::entities::common::style_fixed_height(&t.style).is_none(),
), ),
edit_angle(t!("Rotation").as_ref(), "rotation", t.rotation.to_degrees()), if is_two_point {
ro(
t!("Rotation").as_ref(),
"rotation",
crate::entities::common::format_angle(displayed_rotation(t)),
)
} else {
edit_angle(t!("Rotation").as_ref(), "rotation", t.rotation.to_degrees())
},
edit(t!("Width factor").as_ref(), "width_factor", t.width_factor), edit(t!("Width factor").as_ref(), "width_factor", t.width_factor),
edit_angle(t!("Obliquing").as_ref(), "oblique_angle", t.oblique_angle.to_degrees()), edit_angle(t!("Obliquing").as_ref(), "oblique_angle", t.oblique_angle.to_degrees()),
num_row(t!("Text alignment X").as_ref(), "align_x", ax, align_editable), num_row(t!("Text alignment X").as_ref(), "align_x", ax, align_editable),
@ -451,25 +526,44 @@ fn apply_geom_prop(t: &mut Text, field: &str, value: &str) {
_ => ap.z = v, _ => ap.z = v,
} }
} }
"height" if v > 0.0 => t.height = v, "height"
"rotation" => t.rotation = v.to_radians(), if v > 0.0 && !matches!(t.horizontal_alignment, HA::Aligned) =>
{
t.height = v
}
"rotation" if !matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) => {
t.rotation = v.to_radians()
}
"width_factor" if v > 0.0 => t.width_factor = v, "width_factor" if v > 0.0 => t.width_factor = v,
"oblique_angle" => t.oblique_angle = v.to_radians(), "oblique_angle" if (-85.0..=85.0).contains(&v) => {
t.oblique_angle = v.to_radians()
}
_ => {} _ => {}
} }
} }
fn apply_grip(t: &mut Text, _grip_id: usize, apply: GripApply) { fn apply_grip(t: &mut Text, grip_id: usize, apply: GripApply) {
match apply { match apply {
GripApply::Absolute(p) => { GripApply::Absolute(p) => {
t.insertion_point.x = p.x as f64; let target = if grip_id == 1 {
t.insertion_point.y = p.y as f64; let insertion = t.insertion_point;
t.insertion_point.z = p.z as f64; t.alignment_point.get_or_insert(insertion)
} else {
&mut t.insertion_point
};
target.x = p.x;
target.y = p.y;
target.z = p.z;
} }
GripApply::Translate(d) => { GripApply::Translate(d) => {
t.insertion_point.x += d.x as f64; t.insertion_point.x += d.x;
t.insertion_point.y += d.y as f64; t.insertion_point.y += d.y;
t.insertion_point.z += d.z as f64; t.insertion_point.z += d.z;
if let Some(point) = t.alignment_point.as_mut() {
point.x += d.x;
point.y += d.y;
point.z += d.z;
}
} }
} }
} }
@ -551,7 +645,9 @@ impl Grippable for Text {
value: f64, value: f64,
) { ) {
use crate::scene::model::object::GripMenuAction as A; use crate::scene::model::object::GripMenuAction as A;
if matches!(action, A::RotateText) { if matches!(action, A::RotateText)
&& !matches!(self.horizontal_alignment, HA::Aligned | HA::Fit)
{
self.rotation = value.to_radians(); self.rotation = value.to_radians();
} }
} }