From 0603aae28687346eb58711dcbf6ee5c3e607c8e1 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 26 Aug 2026 22:21:40 +0300 Subject: [PATCH] fix(text): correct scaled text placement Derive two-point layout and continuation spacing from the effective annotation scale. Keep style flags out of per-entity mirror overrides. --- src/app/commands/draw.rs | 14 +++++++++---- src/app/properties.rs | 31 +++++++++++++++++---------- src/app/text_inline.rs | 37 ++++++++++++++++++++------------- src/command.rs | 2 ++ src/entities/text.rs | 24 ++++++++++++++++----- src/modules/annotate/text.rs | 35 ++++++++++++++++++++++++------- src/scene/convert/tessellate.rs | 9 +++++++- src/scene/creation_style.rs | 4 ---- 8 files changed, 109 insertions(+), 47 deletions(-) diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 4c2b3814..72ccacca 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1268,10 +1268,16 @@ impl OpenCADStudio { // ── Annotate commands ────────────────────────────────────────── "TEXT" => { use crate::modules::annotate::text::TextCommand; - let document = &self.tabs[i].scene.document; - let defaults = crate::scene::creation_style::current_text_defaults(document); - let styles = document.text_styles.iter().cloned().collect(); - let new_cmd = TextCommand::with_defaults(defaults, styles); + let (defaults, styles, annotation_multiplier) = { + let scene = &self.tabs[i].scene; + let annotation_multiplier = scene.creation_annotation_multiplier(); + let defaults = + crate::scene::creation_style::current_text_defaults(&scene.document); + let styles = scene.document.text_styles.iter().cloned().collect(); + (defaults, styles, annotation_multiplier) + }; + let new_cmd = + TextCommand::with_defaults(defaults, styles, annotation_multiplier); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } diff --git a/src/app/properties.rs b/src/app/properties.rs index 43d3d463..fde96f93 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1809,12 +1809,30 @@ impl OpenCADStudio { text.horizontal_alignment, acadrust::entities::TextHorizontalAlignment::Aligned ); + let annotative = crate::scene::annotative::is_annotative(doc, entity); + let model_factor = if annotative { + annotation_scale_handle + .and_then(|handle| match doc.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) + } else { + 1.0 + }; let paper_height = if aligned { - crate::entities::text::text_run_placement(text, doc).height as f64 + crate::entities::text::text_run_placement_at_scale( + text, + doc, + model_factor as f32, + ) + .height as f64 } else { text.height }; - let annotative = crate::scene::annotative::is_annotative(doc, entity); for section in sections.iter_mut() { if let Some(row) = section.props.iter_mut().find(|row| row.field == "height") @@ -1830,15 +1848,6 @@ impl OpenCADStudio { } } if annotative { - let model_factor = annotation_scale_handle - .and_then(|handle| match doc.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); insert_row_after( &mut sections, "height", diff --git a/src/app/text_inline.rs b/src/app/text_inline.rs index b08297fc..4af0863f 100644 --- a/src/app/text_inline.rs +++ b/src/app/text_inline.rs @@ -234,6 +234,7 @@ impl super::OpenCADStudio { } else { crate::command::WorkingPlane::default() }; + let command_creation = ed.creation.is_some(); let mut t = if let Some(mut prepared) = ed.creation { prepared.value = ed.value.clone(); prepared @@ -258,10 +259,26 @@ impl super::OpenCADStudio { t.style = cur_style; } } - let annotative = crate::scene::annotative::text_style_is_annotative( - &self.tabs[i].scene.document, - &t.style, - ); + if command_creation { + let annotation_multiplier = if crate::scene::annotative::text_style_is_annotative( + &self.tabs[i].scene.document, + &t.style, + ) { + self.tabs[i].scene.creation_annotation_multiplier() + } else { + 1.0 + }; + let display_height = crate::entities::text::text_run_placement_at_scale( + &t, + &self.tabs[i].scene.document, + annotation_multiplier as f32, + ) + .height as f64 + * annotation_multiplier; + if let Some(command) = self.tabs[i].suspended_cmd.as_mut() { + command.on_editor_display_height(display_height); + } + } self.push_undo_snapshot(i, "TEXT"); self.tabs[i].scene.document.header.current_text_style_name = t.style.clone(); let variable_height = self.tabs[i] @@ -279,17 +296,7 @@ impl super::OpenCADStudio { { self.tabs[i].scene.document.header.text_height = t.height; } - let handle = self.commit_entity_handle(plane.place_entity(EntityType::Text(t))); - if annotative { - let scale = self.tabs[i].scene.current_annotation_scale_handle(); - if let (Some(handle), Some(scale)) = (handle, scale) { - crate::scene::annotative::create_annotation_context( - &mut self.tabs[i].scene.document, - handle, - scale, - ); - } - } + let _ = self.commit_entity_handle(plane.place_entity(EntityType::Text(t))); self.tabs[i].dirty = true; } self.refresh_properties(); diff --git a/src/command.rs b/src/command.rs index 42d81896..c309819d 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1869,6 +1869,8 @@ pub trait CadCommand: Send { /// Resume the command with collected rich text. fn on_editor_text(&mut self, _value: String) {} + fn on_editor_display_height(&mut self, _height: f64) {} + /// Called when the user clicks and `needs_entity_pick()` is true. /// `handle` is the nearest wire's entity handle (Handle::NULL if nothing found). fn on_entity_pick(&mut self, _handle: Handle, _pt: DVec3) -> CmdResult { diff --git a/src/entities/text.rs b/src/entities/text.rs index 48e1ca0e..8e4a504e 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -130,8 +130,12 @@ pub(crate) fn acad_text_encode(value: &str) -> String { out } -fn to_render(t: &Text, document: &acadrust::CadDocument) -> RenderEntity { - let p = text_run_placement(t, document); +pub(crate) fn to_render_at_scale( + t: &Text, + document: &acadrust::CadDocument, + annotation_scale: f32, +) -> RenderEntity { + let p = text_run_placement_at_scale(t, document, annotation_scale); let snap_pt = glam::DVec3::new(p.wcs_insertion[0], p.wcs_insertion[1], p.wcs_insertion[2]); // Parse `%%` codes via acadrust, re-encoded for the stroke tessellator. let value = acad_text_encode(&p.value); @@ -172,7 +176,16 @@ fn to_render(t: &Text, document: &acadrust::CadDocument) -> RenderEntity { /// Compute a TEXT entity's run placement (origin + layout params). Extracted /// from `to_render` verbatim so the stroke and SDF-quad paths agree exactly. -pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPlacement { +pub fn text_run_placement_at_scale( + t: &Text, + document: &acadrust::CadDocument, + annotation_scale: f32, +) -> TextPlacement { + let annotation_scale = if annotation_scale.is_finite() && annotation_scale > 1.0e-9 { + annotation_scale + } else { + 1.0 + }; let normal = (t.normal.x, t.normal.y, t.normal.z); let (wsx, wsy, wsz) = crate::scene::view::transform::ocs_point_to_wcs( ( @@ -222,7 +235,8 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla oblique_angle, ) { if base_bounds.advance > 1.0e-6 { - let scale = (span as f32 / base_bounds.advance).max(1.0e-6); + let scale = + (span as f32 / annotation_scale / base_bounds.advance).max(1.0e-6); if matches!(t.horizontal_alignment, HA::Aligned) { height *= scale; } else { @@ -605,7 +619,7 @@ fn apply_transform(t: &mut Text, tr: &EntityTransform) { impl RenderConvertible for Text { fn to_render(&self, document: &acadrust::CadDocument) -> Option { - Some(to_render(self, document)) + Some(to_render_at_scale(self, document, 1.0)) } } diff --git a/src/modules/annotate/text.rs b/src/modules/annotate/text.rs index 041394cb..90d815a6 100644 --- a/src/modules/annotate/text.rs +++ b/src/modules/annotate/text.rs @@ -47,12 +47,17 @@ pub struct TextCommand { oblique_angle: f64, fixed_height: bool, annotative: bool, - generation_flags: i16, + annotation_multiplier: f64, + last_display_height: Option, last_entity: Option, } impl TextCommand { - pub fn with_defaults(defaults: TextCreationDefaults, styles: Vec) -> Self { + pub fn with_defaults( + defaults: TextCreationDefaults, + styles: Vec, + annotation_multiplier: f64, + ) -> Self { let current_height = defaults.height; let mut command = Self { step: Step::Start, @@ -69,7 +74,8 @@ impl TextCommand { oblique_angle: defaults.oblique_angle, fixed_height: false, annotative: false, - generation_flags: 0, + annotation_multiplier: annotation_multiplier.max(1.0e-9), + last_display_height: None, last_entity: None, }; let style = command.style_name.clone(); @@ -102,8 +108,6 @@ impl TextCommand { 85.0_f64.to_radians(), ); self.annotative = style.annotative; - self.generation_flags = (if style.flags.backward { 2 } else { 0 }) - | (if style.flags.upside_down { 4 } else { 0 }); true } @@ -166,7 +170,6 @@ impl TextCommand { text.rotation = self.rotation; text.horizontal_alignment = self.horizontal; text.vertical_alignment = self.vertical; - text.generation_flags = self.generation_flags; text.alignment_point = if self.is_two_point() { let second = self.plane.to_local(self.second_point?); Some(Vector3::new(second.x, second.y, second.z)) @@ -183,6 +186,7 @@ impl TextCommand { return CmdResult::NeedPoint; }; let pos = self.first_point.unwrap_or(DVec3::ZERO); + self.last_display_height = None; self.last_entity = Some(entity.clone()); CmdResult::SuspendForTextInput { pos, entity } } @@ -199,7 +203,18 @@ impl TextCommand { } else { entity.rotation }; - let spacing = entity.height.max(1.0e-9) * 1.666_666_666_7; + let fallback_height = entity.height.max(1.0e-9) + * if self.annotative { + self.annotation_multiplier + } else { + 1.0 + }; + let spacing = self + .last_display_height + .take() + .unwrap_or(fallback_height) + .max(1.0e-9) + * 1.666_666_666_7; let delta = Vector3::new(angle.sin() * spacing, -angle.cos() * spacing, 0.0); entity.insertion_point = entity.insertion_point + delta; if let Some(point) = entity.alignment_point.as_mut() { @@ -422,6 +437,12 @@ impl CadCommand for TextCommand { } } + fn on_editor_display_height(&mut self, height: f64) { + if height.is_finite() && height > 1.0e-9 { + self.last_display_height = Some(height); + } + } + fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index d474c5f1..a50f35c4 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -633,7 +633,14 @@ pub fn tessellate( // stay a roughly constant on-screen size; otherwise the header-driven path. let te = crate::entities::point::relative_render(entity, document, world_per_pixel) .or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel)) - .or_else(|| convert(entity, document)); + .or_else(|| match entity { + EntityType::Text(text) => Some(crate::entities::text::to_render_at_scale( + text, + document, + anno_scale, + )), + _ => convert(entity, document), + }); if let Some(te) = te { match te.object { // ── Text / MText: pre-tessellated glyph strokes ─────────────── diff --git a/src/scene/creation_style.rs b/src/scene/creation_style.rs index ed4a1fc5..bfc4ac5c 100644 --- a/src/scene/creation_style.rs +++ b/src/scene/creation_style.rs @@ -134,10 +134,6 @@ fn apply_text_defaults(doc: &CadDocument, entity: &mut EntityType) { if text.oblique_angle.abs() <= 1.0e-9 { text.oblique_angle = resolved.oblique_angle; } - if text.generation_flags == 0 { - text.generation_flags = (if resolved.flags.backward { 2 } else { 0 }) - | (if resolved.flags.upside_down { 4 } else { 0 }); - } } EntityType::MText(text) => { if resolved.height > 1.0e-9 {